Next round in 3s
1.00x
πŸ”₯
0
Streak
0
km/h
0
metres
KES 0
potential
β€”
β›½
TANK EMPTY!
You're out of fuel. Top up your tank to keep driving and winning.
$0.00
0
β€”
β€”
0 L
β€”x
Login or register to stake, buy fuel & win real money
TOP DRIVERS
Loading…
COMMUNITY CHAT
MY ACCOUNT

Login to manage your account

ℹ️
Notice
LOG IN

Enter the email address on your account and we'll send you a reset link.

Have account? Log in
πŸ“± M-Pesa (Equity)
πŸ“± M-Pesa
πŸ’³ PayPal

βœ… Recommended. STK Push via Equity Jenga API β€” enter your M-Pesa PIN on your phone. Funds credit instantly.

After initiating prompt, a popup will show on your phone. Enter your M-Pesa PIN to confirm and recharge instantly.

You will be redirected to PayPal. Amount is charged in USD equivalent. After payment you are returned here automatically.

Available to withdraw: KES 0
Referral fuel earnings are non-withdrawable.

πŸ“± M-Pesa (Equity)
πŸ“± M-Pesa
πŸ’³ PayPal

βœ… Recommended. Funds sent directly to M-Pesa via Equity Jenga API β€” arrives within minutes.

Fuel is consumed each round. Buying 1x costs 1L, reaching 2x costs 2L total, etc.

Total cost: KES 0 from your cash balance

Loading profile…
1 β€” DEPOSIT FUNDS

Deposit KES via M-Pesa or Equity Jenga. Your cash balance appears in the top header after funds arrive.

2 β€” BUY FUEL β›½

Fuel powers your car. Convert your cash balance into fuel litres (1L = KES 26). You need at least 1L to play. Your fuel tank is shown in the control bar.

3 β€” PLACE YOUR STAKE

Enter your stake amount (in KES) and click STAKE & DRIVE. Your stake is immediately deducted from your cash balance.

4 β€” WATCH THE MULTIPLIER GROW

After the countdown, your car starts driving. The multiplier climbs from 1.00x upwards. Your potential payout = stake Γ— current multiplier.

5 β€” CASH OUT BEFORE CRASH βœ…

Hit CASH OUT at any time to lock in your winnings. Your payout lands in your cash balance instantly. Miss it and the engine blows β€” you lose your stake.

6 β€” CRASH = STAKE LOST πŸ’₯

If the engine crashes before you cash out, your stake is lost. Fuel used during the round is also consumed. Set an Auto Exit multiplier to cash out automatically.

⚑ TURBO MODE

Activate Turbo before the round starts for faster multiplier growth. Higher reward but the crash can come sooner. Uses fuel faster too.

Is the crash point predetermined? β–Ύ

Yes. Every round uses a provably fair algorithm (HMAC-SHA256). The crash point is fixed before betting opens. You can verify any round using the server seed revealed after each round.

How quickly does M-Pesa deposit arrive? β–Ύ

M-Pesa deposits via STK Push usually reflect in under 30 seconds after you enter your PIN. If it takes longer, check your M-Pesa messages β€” the payment may have been declined by Safaricom.

What is fuel and why do I need it? β–Ύ

Fuel is your car's energy. You need at least 1 litre to start a round. As your multiplier climbs, fuel is consumed (1L per full multiplier step). When your tank hits zero, the car automatically cashes out β€” protecting you from going past your fuel range.

Can I withdraw anytime? β–Ύ

Yes. Withdrawals are processed via M-Pesa B2C or PayPal and typically arrive within 1–3 minutes. Minimum withdrawal is KES 100. Referral fuel cannot be withdrawn β€” only real stakes and winnings are withdrawable cash.

What is the house edge? β–Ύ

The platform retains a 3% house edge. This is baked into the crash point generation formula β€” over many rounds, the expected payout to players is 97% of total stakes.

How does the referral program work? β–Ύ

Share your referral link. When a friend signs up via your link, you get +1L free fuel. When they make their first deposit, you earn +5L. Referral fuel can be used to play but cannot be withdrawn as cash.

What happens if the page crashes mid-round? β–Ύ

If you refresh during a live round, your bet is safely stored server-side. On your next login the stale bet is cleared automatically. Your wallet is always the authoritative record β€” reload and check your balance.

I set Auto Exit but it didn't fire β€” why? β–Ύ

Auto Exit fires only if the multiplier reaches your target before the crash. If the engine crashes below your auto exit value, the round ends and you lose your stake. Set a realistic auto exit β€” values like 1.5x or 2x give you a much higher chance of success.

// ══ LAMBORGHINI HURACÁN CAR (SVG image on canvas) ══════════════════════════════ var _carImg = null, _carImgTurbo = null; function _getCarImg(turbo, cb) { var cache = turbo ? '_carImgTurbo' : '_carImg'; if (window[cache]) { cb(window[cache]); return; } var svgN = ""; var svgT = ""; var svg = turbo ? svgT : svgN; var img = new Image(); img.onload = function() { window[cache] = img; cb(img); }; img.onerror = function(e) { console.error('Car SVG load failed', e); }; img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg); } function drawCar(cx, cy, rot, crashed, turbo) { if (crashed && G.crashPhase >= 3) { X.save(); X.translate(cx, cy); X.font = '40px serif'; X.textAlign = 'center'; X.textBaseline = 'middle'; X.fillText('\uD83D\uDD25', 0, 0); X.restore(); return; } var W = 120, H = 44; _getCarImg(turbo, function(img) { X.save(); X.translate(cx, cy); if (rot) X.rotate(rot); X.fillStyle = 'rgba(0,0,0,0.22)'; X.beginPath(); X.ellipse(2, H * 0.58, W * 0.46, H * 0.15, 0, 0, Math.PI * 2); X.fill(); X.drawImage(img, -W / 2, -H / 2, W, H); if (turbo) { X.strokeStyle = 'rgba(255,180,0,0.5)'; X.lineWidth = 1.8; [-12, -5, 3].forEach(function(oy) { X.beginPath(); X.moveTo(-W/2 - 22, oy); X.lineTo(-W/2 - 5, oy); X.stroke(); }); } X.restore(); }); } // ── PARTICLES ──────────────────────────────────────────── function spawnExhaust() { exhaust.push({ x:G.carX-28+Math.random()*4, y:G.carY+8, vx:-Math.random()*2.2-0.4, vy:(Math.random()-0.5)*1.3, life:1, sz:Math.random()*5+2, col: G.turbo ? '#f0a11a' : '#710ba3', }); } function spawnSparks(x,y,n) { n=n||10; for(var i=0;i-20&&ob.x0?1:-1)*0.07; cx=G.carX; cy=G.carY; if(G.crashPhase>=2) spawnFlame(cx,cy-8); if(G.crashPhase===1&&Math.random()<0.3) spawnSparks(cx,cy,2); } // Car glow if (moving) { var cg=X.createRadialGradient(cx,cy,1,cx,cy,62); cg.addColorStop(0,G.turbo?'rgba(240,161,26,0.38)':'rgba(232,160,32,0.28)'); cg.addColorStop(1,'transparent'); X.fillStyle=cg; X.beginPath(); sarc(cx,cy,62,0,Math.PI*2); X.fill(); } // Draw car drawCar(cx, cy, G.phase==='crashed'?G.carRot:0, G.phase==='crashed', G.turbo); // Exhaust if (moving&&Math.random()<0.55) spawnExhaust(); exhaust=exhaust.filter(function(p){return p.life>0;}); exhaust.forEach(function(p){ p.x+=p.vx; p.y+=p.vy; p.life-=0.055; var r=Math.max(0.01,p.sz*p.life); X.globalAlpha=Math.max(0,p.life); X.fillStyle=p.col; X.beginPath(); sarc(p.x,p.y,r,0,Math.PI*2); X.fill(); }); X.globalAlpha=1; // Sparks sparks=sparks.filter(function(s){return s.life>0;}); sparks.forEach(function(s){ s.x+=s.vx; s.y+=s.vy; s.vy+=0.2; s.life-=0.055; var r=Math.max(0.01,s.sz*s.life); X.globalAlpha=Math.max(0,s.life); X.fillStyle='hsl('+(30+Math.floor(Math.random()*30))+',100%,65%)'; X.beginPath(); sarc(s.x,s.y,r,0,Math.PI*2); X.fill(); }); X.globalAlpha=1; // Flames flames=flames.filter(function(f){return f.life>0;}); flames.forEach(function(f){ f.y+=f.vy; f.life-=0.045; var r=Math.max(0.01,f.sz*f.life); X.globalAlpha=Math.max(0,f.life*0.8); var fg=X.createRadialGradient(f.x,f.y,0,f.x,f.y,r); fg.addColorStop(0,'rgba(255,245,60,0.95)'); fg.addColorStop(0.5,'rgba(255,70,10,0.75)'); fg.addColorStop(1,'transparent'); X.fillStyle=fg; X.beginPath(); sarc(f.x,f.y,r,0,Math.PI*2); X.fill(); }); X.globalAlpha=1; // Crash flash if(G.phase==='crashed'&&G.crashPhase>=1){ X.fillStyle='rgba(255,30,30,'+Math.max(0,0.18-G.crashPhase*0.04)+')'; X.fillRect(0,0,W,H); } requestAnimationFrame(drawFrame); } drawFrame(); // ════════════════════════════════════════════════════════ // LIVE GAME LOOP β€” runs always, seamless sim ↔ real // Phases: 'sim_cd' β†’ 'sim' β†’ 'crashed' β†’ (pause) β†’ 'sim_cd' ... // Real bet phases: 'cd' β†’ 'running' β†’ 'crashed' β†’ 'idle' β†’ back to sim // ════════════════════════════════════════════════════════ function genCrash() { if(Math.random()<0.04) return 1.00; var raw = Math.max(1.00,parseFloat((0.97/(1-Math.random())).toFixed(4))); return Math.min(raw, MAX_MULT); } // Fake player cashouts during sim β€” fires at random multipliers before crash var SIM_CASHOUT_POOL = []; // pending fake cashouts this round var SIM_ACTIVE_NAMES = []; // names used this round (reset each round) function pickUniqueName() { // Pick a name not already active this round var available = NAMES.filter(function(n){ return SIM_ACTIVE_NAMES.indexOf(n) < 0; }); if (!available.length) available = NAMES; // full reset if exhausted var name = available[Math.floor(Math.random()*available.length)]; SIM_ACTIVE_NAMES.push(name); return name; } function simScheduleFakeCashouts(crashAt) { SIM_CASHOUT_POOL = []; SIM_ACTIVE_NAMES = []; var count = Math.floor(3 + Math.random()*5); // 3-7 players per round for(var i=0;i0?'Next round in '+simCd+'s':'🟒 GO!'); if(simCd<=0){ clearInterval(simCdInt); runSim(); } },1000); } function runSim() { if(G.phase==='cd'||G.phase==='running') return; // don't interrupt real round G.phase='sim'; G.simMult=1; G.simCrash=genCrash(); G.simT0=Date.now(); G.carX=CV.width*0.27; G.carY=G.carBaseY||CV.height*0.53; exhaust=[]; sparks=[]; flames=[]; setPill('pill-live','🟒 LIVE'); // just LIVE, no SIMULATOR setMV('1.00x','mv-safe',''); el('rnum').textContent='β€”'; el('rfill').style.strokeDashoffset=138.2; simScheduleFakeCashouts(G.simCrash); clearInterval(simInt); simInt=setInterval(simTick,50); } function simTick() { var el2=(Date.now()-G.simT0)/1000; G.simMult=Math.min(G.simCrash,parseFloat((1+el2*0.044*G.simMult).toFixed(4))); var cls=G.simMult>=5?'mv-hot':G.simMult>=2?'mv-warn':'mv-safe'; setMV(G.simMult.toFixed(2)+'x',cls,''); // no label ever el('hspd').textContent=Math.round(G.simMult*118); el('hdst').textContent=Math.round(el2*G.simMult*50); el('hpot').textContent=kesF(5*G.simMult); // Fire fake cashouts at their target multipliers SIM_CASHOUT_POOL.forEach(function(p){ if(!p.fired && G.simMult>=p.target){ p.fired=true; addFeed(p.name, p.target, true); } }); if(G.simMult>=G.simCrash) simCrash2(); } function simCrash2() { clearInterval(simInt); G.phase='crashed'; var hits=G.obstacles.filter(function(o){return !o.hit&&o.x>G.carX-20&&o.x 1.02){ var w=Math.random()>0.45; // Win: random mult between 1.02 and current live mult (already passed, realistic) // Loss: show the crash mult only if the round just crashed (handled in simCrash2) // Here we only show wins from "other players who cashed out earlier this round" if(w){ var m=parseFloat((1.02+Math.random()*(G.simMult-1.02)).toFixed(2)); addFeed(pickUniqueName(),m,true); } } }, 4000+Math.random()*2000); // Viewer count setInterval(function(){ var v=el('simViewers'); if(v) v.textContent=Math.floor(18+Math.random()*45); },4000); // ════════════════════════════════════════════════════════ // REAL GAME LOGIC // ════════════════════════════════════════════════════════ function setPill(cls2,txt) { var p=el('pill'); p.className='pill '+cls2; p.textContent=txt; } function setMV(v,cls2,lbl) { el('mv').textContent=v; el('mv').className='mv '+cls2; // Never show any label text β€” clean look, no 'SIMULATOR', 'WAITING', etc. el('ml').textContent=''; } function setRing(v,mx) { var circ=138.2, off=v<=0?circ:Math.max(0,circ-(v/mx)*circ); el('rfill').style.strokeDashoffset=off; el('rnum').textContent=v>0?Math.ceil(v):'β€”'; } function updateFuelBar() { var pct=Math.max(0,Math.min(100,(S.fuel/Math.max(1,S.maxFuel))*100)); el('fgf').style.width=pct+'%'; el('fgv').textContent=S.fuel.toFixed(1)+' L'; } function updateWallet() { var cashStr=kesF(S.cash); ['cashD','acash'].forEach(function(id){var e=el(id);if(e)e.textContent=cashStr;}); ['fuelD','afuel'].forEach(function(id){var e=el(id);if(e)e.textContent=S.fuel.toFixed(1)+' L';}); var aw=el('awith'); if(aw)aw.textContent=cashStr; var wa=el('wdAvail'); if(wa)wa.textContent=cashStr; updateFuelBar(); } // ── SESSION STATS ──────────────────────────────────────── var SS = { rounds:0, wins:0, losses:0, pnl:0, fuelUsed:0, bestMult:0 }; function updateDash() { var pnlEl=el('dPnl'); if(pnlEl){ pnlEl.textContent=(SS.pnl>=0?'+':'')+kesF(SS.pnl); pnlEl.style.color=SS.pnl>=0?'var(--gr)':'var(--rd)'; } var re=el('dRounds'); if(re)re.textContent=SS.rounds; var we=el('dWinRate'); if(we)we.textContent=SS.rounds>0?Math.round(SS.wins/SS.rounds*100)+'%':'β€”'; var be=el('dBest'); if(be)be.textContent=SS.bestMult>0?SS.bestMult.toFixed(2)+'x':'β€”'; var fe=el('dFuel'); if(fe)fe.textContent=SS.fuelUsed.toFixed(1)+' L'; var mm=el('dMaxMult'); if(mm)mm.textContent=MAX_MULT+'x'; } // ── DASHBOARD TOGGLE ───────────────────────────────────── var dashOpen=false; function toggleDash() { dashOpen=!dashOpen; var p=el('dashPanel'), b=el('btnDash'); if(p)p.className='dash-panel'+(dashOpen?' dash-panel-on':''); if(b)b.className='btn-dash'+(dashOpen?' btn-dash-on':''); if(dashOpen)updateDash(); } // ── FUEL ALERT ─────────────────────────────────────────── function showFuelAlert() { showAlert('β›½','Tank Empty!','Buy fuel to keep driving and winning.','warn'); var a=el('fuelAlert'); if(a)a.className='fuel-alert fuel-alert-on'; } function hideFuelAlert() { dismissAlert(); var a=el('fuelAlert'); if(a)a.className='fuel-alert'; } function doStake() { if(!S.loggedIn){openModal('login');return;} var sv=parseFloat(el('stakeIn').value)||10; var av=parseFloat(el('autoIn').value)||null; if(sv<10){showErr('Minimum stake is KES 10');return;} if(sv>S.cash){showErr('Insufficient cash balance. Available: '+kesF(S.cash));return;} if(S.fuel<1){showFuelAlert();return;} G.stake=sv; G.cashedOut=false; G.turbo=false; G.fuelConsumed=0; // Stop simulator and countdown cleanly β€” real round taking over clearInterval(simInt); clearInterval(simCdInt); if(G.phase==='sim'||G.phase==='sim_cd'||G.phase==='idle') { G.phase='idle'; G.crashPhase=0; exhaust=[]; sparks=[]; flames=[]; G.carX=CV.width*0.27; G.carY=G.carBaseY; } // Disable controls while waiting for server confirmation el('btnS').style.display='none'; el('btnC').style.display='block'; el('btnT').style.display='block'; el('stakeIn').disabled=true; el('autoIn').disabled=true; genObs(); // Let server deduct cash β€” sync balances from authoritative response postApi('/api/place_bet',{stake:sv,auto_cashout:av},function(d){ if(d&&d.success){ G.betId=d.bet_id; G.roundId=d.round_id; S.cash=d.cash_balance; S.fuel=d.fuel_balance; updateWallet(); doCountdown(); } else { // API failed β€” restore controls el('btnS').style.display='block'; el('btnC').style.display='none'; el('btnT').style.display='none'; el('stakeIn').disabled=false; el('autoIn').disabled=false; showErr(d&&d.error?d.error:'Could not place bet. Check your connection.'); } }); } function doCountdown() { G.phase='cd'; G.cd=5; setPill('pill-cd','Round starts in 5s'); setMV('1.00x','mv-idle','GET READY'); setRing(5,5); clearInterval(cdInt); cdInt=setInterval(function(){ G.cd--; setRing(G.cd,5); setPill('pill-cd',G.cd>0?'Round starts in '+G.cd+'s':'🟒 GO!'); if(G.cd<=0){clearInterval(cdInt);startDriving();} },1000); } function startDriving() { G.phase='running'; G.mult=1; G.crashAt=genCrash(); t0=Date.now(); G.carX=CV.width*0.27; G.carY=G.carBaseY; G.fuelConsumed=0; exhaust=[]; sparks=[]; flames=[]; setPill('pill-live','🟒 DRIVING'); setMV('1.00x','mv-safe','MULTIPLIER'); el('rnum').textContent='β€”'; el('rfill').style.strokeDashoffset=138.2; clearInterval(gInt); gInt=setInterval(gameTick,50); } function gameTick() { var elapsed=(Date.now()-t0)/1000; var rate=G.turbo?0.068:0.044; G.mult=Math.min(G.crashAt,parseFloat((1+elapsed*rate*G.mult).toFixed(4))); // Fuel: floor(mult) litres consumed total this round var fuelNeeded=Math.max(1,Math.floor(G.mult)); var extra=fuelNeeded-G.fuelConsumed; if(extra>0){S.fuel=Math.max(0,parseFloat((S.fuel-extra).toFixed(2)));G.fuelConsumed=fuelNeeded;updateFuelBar();} var cls2=G.mult>=5?'mv-hot':G.mult>=2?'mv-warn':'mv-safe'; setMV(G.mult.toFixed(2)+'x',cls2,'MULTIPLIER'); el('hspd').textContent=Math.round(G.mult*118); el('hdst').textContent=Math.round(elapsed*G.mult*50); el('hpot').textContent=kesF(G.stake*G.mult); // Auto cashout var au=parseFloat(el('autoIn').value); if(!G.cashedOut&&au>=1.01&&G.mult>=au){doCO('auto');return;} // Out of fuel β€” forced auto-exit if(S.fuel<=0){doCO('fuel');return;} // Crash if(G.mult>=G.crashAt){triggerCrash();} } function doCO(reason) { if(G.phase!=='running'||G.cashedOut) return; G.cashedOut=true; clearInterval(gInt); var capturedRoundId=G.roundId; // Fuel-empty: player gets stake BACK ONLY β€” no profit if(reason==='fuel'){ G.streak=0; // no streak for running dry SS.rounds++; SS.losses++; SS.pnl=parseFloat((SS.pnl+0).toFixed(2)); // break-even from platform view SS.fuelUsed+=G.fuelConsumed; updateDash(); if(G.betId||G.roundId){ postApi('/api/fuel_empty',{bet_id:G.betId||0,round_id:G.roundId||0,fuel_used:G.fuelConsumed||1},function(d){ if(G.roundId!==capturedRoundId) return; if(d&&d.success){ S.cash=d.cash_balance; S.fuel=d.fuel_balance; updateWallet(); setTimeout(loadHistoryFromDB, 600); setTimeout(loadActivityFromDB, 700); } else { getApi('/api/game_state',function(gs){ if(G.roundId!==capturedRoundId) return; if(gs&&gs.wallet){S.cash=gs.wallet.cash;S.fuel=gs.wallet.fuel;updateWallet();} }); } }); } showPopup('lose','β›½ Stake back: '+kesF(G.stake)); addHistory(1.00,G.stake,false); addFeed('You',1.00,false); setMV('1.00x','mv-hot','β›½ TANK EMPTY'); setPill('pill-crash','β›½ Out of fuel β€” stake refunded'); setTimeout(showFuelAlert, 2000); setTimeout(endRound,3500); return; } // Normal cashout (manual or auto-exit) β€” player wins at current mult var win=parseFloat((G.stake*G.mult).toFixed(2)); G.streak++; el('streakN').textContent=G.streak; SS.rounds++; SS.wins++; SS.pnl=parseFloat((SS.pnl+(win-G.stake)).toFixed(2)); SS.fuelUsed+=G.fuelConsumed; if(G.mult>SS.bestMult)SS.bestMult=G.mult; updateDash(); if(G.betId){ postApi('/api/cashout',{bet_id:G.betId,mult:G.mult,fuel_used:G.fuelConsumed||1},function(d){ if(G.roundId!==capturedRoundId) return; if(d&&d.success){ S.cash=d.cash_balance; S.fuel=d.fuel_balance; updateWallet(); } else { getApi('/api/game_state',function(gs){ if(G.roundId!==capturedRoundId) return; if(gs&&gs.wallet){S.cash=gs.wallet.cash;S.fuel=gs.wallet.fuel;updateWallet();} }); } }); } else { S.cash=parseFloat((S.cash+win).toFixed(2)); updateWallet(); } showPopup('win','+'+kesF(win)); addHistory(G.mult,win,true); addFeed('You',G.mult,true); if(reason==='auto'){ setMV(G.mult.toFixed(2)+'x','mv-safe','🎯 AUTO EXIT'); setPill('pill-idle','Auto exit hit!'); } else { setMV(G.mult.toFixed(2)+'x','mv-safe','βœ… EXITED'); setPill('pill-idle','Cashed out!'); } setTimeout(loadHistoryFromDB, 600); setTimeout(loadActivityFromDB, 700); setTimeout(endRound,2000); } function triggerCrash() { clearInterval(gInt); G.phase='crashed'; G.mult=parseFloat(G.crashAt.toFixed(2)); setMV(G.mult.toFixed(2)+'x','mv-hot','πŸ’₯ CRASHED'); setPill('pill-crash','πŸ’₯ ENGINE BLOW'); var candidates=G.obstacles.filter(function(o){return !o.hit&&o.x>G.carX-20&&o.x=3?'hbdg-g':m>=1.5?'hbdg-a':'hbdg-r'; return '
'+ ''+(label||'#'+roundIdx)+''+ ''+m.toFixed(2)+'x'+ ''+ (win?'+':'-')+kesF(amt)+ '
'; } function addHistory(mult,amt,win) { roundIdx++; var h=renderHistoryRow(mult,amt,win,'#'+roundIdx); [el('tp-hist'),el('mhl')].forEach(function(e){ if(!e) return; if(e.querySelector('p'))e.innerHTML=''; e.insertAdjacentHTML('afterbegin',h); if(e.children.length>25)e.removeChild(e.lastChild); }); } function loadHistoryFromDB() { if(!S.loggedIn) return; getApi('/api/history',function(d){ [el('tp-hist'),el('mhl')].forEach(function(e){ if(!e) return; if(!d||!d.success||!d.history||!d.history.length){ e.innerHTML='

No rounds yet β€” play a round to see history

'; return; } e.innerHTML=''; // history is newest-first from API d.history.forEach(function(b){ var win=(b.result==='won'); var mult=win?parseFloat(b.cashout_mult||1):parseFloat(b.crash_at||1); var amt=win?parseFloat(b.payout_usd):parseFloat(b.stake_usd); e.insertAdjacentHTML('beforeend',renderHistoryRow(mult,amt,win,'#'+b.round_id)); }); }); }); } function loadActivityFromDB() { if(!S.loggedIn) return; getApi('/api/activity',function(d){ var targets=[el('tp-actv'),el('actvList'),el('actvListM')]; if(!d||!d.success||!d.activity||!d.activity.length){ targets.forEach(function(e){ if(e) e.innerHTML='

No activity yet

'; }); return; } var icons={deposit:'πŸ’³',withdrawal:'πŸ’Έ',fuel_purchase:'β›½',bet_placed:'🎯',bet_won:'πŸ†',referral_fuel:'🎁',admin_credit:'⭐',admin_debit:'⚠️'}; var credits={deposit:1,bet_won:1,referral_fuel:1,admin_credit:1}; var html=''; d.activity.forEach(function(tx){ var icon=icons[tx.type]||'πŸ“‹'; var isCr=!!credits[tx.type]; var amtCls=isCr?'actv-amt-cr':(tx.type==='fuel_purchase'?'actv-amt-ne':'actv-amt-db'); var amtPfx=isCr?'+':'-'; var cur=tx.currency==='FUEL'?'L':''; var amt=tx.currency==='FUEL' ? parseFloat(tx.amount).toFixed(1)+' L' : kesF(tx.amount); var note=tx.note||tx.type.replace(/_/g,' '); // Format time: "12 Jan 14:32" var dt=new Date(tx.created_at.replace(' ','T')); var months=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; var timeStr=dt.getDate()+' '+months[dt.getMonth()]+' '+ String(dt.getHours()).padStart(2,'0')+':'+String(dt.getMinutes()).padStart(2,'0'); html+='
'+ ''+icon+''+ ''+note+''+ ''+amtPfx+amt+''+ ''+timeStr+''+ '
'; }); targets.forEach(function(e){ if(e) e.innerHTML=html; }); }); } function addFeed(name,mult,win) { var f=el('feed'), d=document.createElement('div'); d.className='feedrow'; d.innerHTML=''+name+''+(win?mult.toFixed(2)+'x βœ…':'CRASHED πŸ’₯')+''; f.insertBefore(d,f.firstChild); if(f.children.length>5)f.removeChild(f.lastChild); } // ── CHAT ───────────────────────────────────────────────── var chatLastId=0; var CHAT_COLORS=['#a78bfa','#34d399','#fb923c','#f472b6','#60a5fa','#facc15','#4ade80','#f87171']; function chatColor(name){ var h=0; for(var i=0;i'+escHtml(m.username)+''+ ''+chatFmt(m.created_at)+''+ ''; var bubbleBg=isMe ?'background:var(--pu);color:#fff;border-bottom-right-radius:4px' :'background:'+col+'22;border:1px solid '+col+'55;color:#fff;border-bottom-left-radius:4px'; return '
'+ nameLine+ '
'+escHtml(m.message)+'
'+ '
'; } function escHtml(s){ return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function loadChat(initial){ var url='/api/chat'+((!initial&&chatLastId>0)?'?since='+chatLastId:''); getApi(url,function(d){ if(!d||!d.success||!d.messages) return; var box=el('chatMsgs'); if(!box) return; var atBottom=box.scrollHeight-box.scrollTop-box.clientHeight<40; var msgs=d.messages; if(initial){ box.innerHTML=''; } msgs.forEach(function(m){ if(m.id<=chatLastId) return; chatLastId=Math.max(chatLastId,parseInt(m.id)); var isMe=S.loggedIn&&m.username===S.username; var existing=box.querySelector('[data-id="'+m.id+'"]'); if(!existing) box.insertAdjacentHTML('beforeend',renderChatMsg(m,isMe)); }); if(initial||atBottom) box.scrollTop=box.scrollHeight; }); } function sendChat(){ if(!S.loggedIn){openModal('login');return;} var inp=el('chatInput'); var msg=inp.value.trim(); if(!msg) return; inp.value=''; // Close emoji picker after sending var ep=el('emojiPicker'); if(ep)ep.classList.remove('ep-on'); postApi('/api/chat',{message:msg},function(d){ if(d&&d.success){ loadChat(false); } else if(d&&d.error){ showToast('❌ '+d.error,'err'); } }); } function toggleEmojiPicker(){ var ep=el('emojiPicker'); if(ep)ep.classList.toggle('ep-on'); } function insertEmoji(emoji){ var inp=el('chatInput'); if(!inp) return; var pos=inp.selectionStart||inp.value.length; inp.value=inp.value.slice(0,pos)+emoji+inp.value.slice(pos); inp.focus(); inp.selectionStart=inp.selectionEnd=pos+emoji.length; } // Close emoji picker on click outside document.addEventListener('click',function(e){ var ep=el('emojiPicker'); var tog=el('emojiToggle'); if(ep&&ep.classList.contains('ep-on')&&!ep.contains(e.target)&&e.target!==tog) ep.classList.remove('ep-on'); }); // Poll chat every 4 seconds setInterval(function(){ if(chatLastId>=0) loadChat(false); },4000); // Initial load on page open (guests can read chat) setTimeout(function(){ loadChat(true); },1000); // ── POPUPS ──────────────────────────────────────────────── function showPopup(type,txt) { var p=type==='win'?el('popW'):el('popL'); var a=type==='win'?el('popWA'):el('popLA'); a.textContent=txt; p.className=p.className.replace(' popup-show','')+' popup-show'; setTimeout(function(){p.className=p.className.replace(' popup-show','');},2800); } // ── API HELPER ──────────────────────────────────────────── function postApi(url,body,cb) { fetch(API_BASE+url,{ method:'POST', credentials:'include', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) }) .then(function(r){ var ct=r.headers.get('content-type')||''; if(!ct.includes('application/json')) throw new Error('Server error ('+r.status+')'); return r.json(); }) .then(function(d){if(cb)cb(d);}) .catch(function(e){ console.warn('API error:',url,e); if(cb)cb({success:false,error:e.message||'Network error. Please try again.'}); }); } function getApi(url,cb) { fetch(API_BASE+url,{credentials:'include'}) .then(function(r){ var ct=r.headers.get('content-type')||''; if(!ct.includes('application/json')) throw new Error('Server error ('+r.status+')'); return r.json(); }) .then(function(d){if(cb)cb(d);}) .catch(function(e){console.warn('API error:',url,e);if(cb)cb(null);}); } // ── WALLET POLL (5s) ───────────────────────────────────── setInterval(function(){ if(!S.loggedIn) return; // Don't overwrite wallet mid-round β€” wait until round is settled if(G.phase==='running'||G.phase==='cd') return; var pollRoundId=G.roundId; getApi('/api/game_state',function(d){ // Discard if a round started while this request was in flight if(G.phase==='running'||G.phase==='cd') return; if(G.roundId!==pollRoundId) return; if(d&&d.success&&d.wallet){ S.cash=d.wallet.cash; S.fuel=d.wallet.fuel; if(d.wallet.fuel>S.maxFuel) S.maxFuel=d.wallet.fuel; updateWallet(); } }); },5000); // ── ACTIVITY POLL (30s) β€” catches async M-Pesa/PayPal callbacks ── setInterval(function(){ if(!S.loggedIn) return; loadActivityFromDB(); },30000); // ════════════════════════════════════════════════════════ // AUTH // ════════════════════════════════════════════════════════ function onLogin(user) { S.loggedIn=true; S.username=user.username||''; S.cash=user.cash||0; S.fuel=user.fuel||0; S.maxFuel=Math.max(30,S.fuel); S.refCode=user.refCode||''; S.refCount=user.refCount||0; S.refFuel=user.refFuel||0; // Header el('hdrR').innerHTML= '
'+ '
Cash
'+kesF(S.cash)+'
'+ '
'+ '
Fuel
'+S.fuel.toFixed(1)+' L
'+ '
'+ ''+ ''+ '
'+ 'πŸ‘€'+ '
'+ '
'; // Show real controls, hide auth gate and sim banner el('simbanner').style.display='none'; el('gw').classList.add('no-banner'); el('cbar').style.display='flex'; el('authgate').style.display='none'; // The game is already running (sim loop) β€” don't stop it. // Just update the pill to invite them to stake. Sim continues seamlessly. if(G.phase==='sim'||G.phase==='sim_cd'){ if(S.fuel<1){ setPill('pill-idle','Buy fuel to start playing β›½'); } else { setPill('pill-idle','Place your stake & drive!'); } var pill=el('pill'); if(pill)pill.className='pill pill-idle'; } // Account panel el('acct-guest').style.display='none'; el('acct-user').style.display='block'; var rc=el('refCode'); if(rc)rc.textContent=S.refCode; var rct=el('refCount'); if(rct)rct.textContent=S.refCount; var rfl=el('refFuel'); if(rfl)rfl.textContent=S.refFuel.toFixed(1)+' L'; updateWallet(); // Load persisted data setTimeout(loadHistoryFromDB, 300); setTimeout(loadActivityFromDB, 400); setTimeout(loadLeaderboard, 500); } function onGuest() { el('hdrR').innerHTML= ''+ ''+ ''; startSimRound(); // start seamless live-looking loop } // Init from PHP session if (INITIAL_USER) { onLogin(INITIAL_USER); } else { onGuest(); } // Show deposit/payment result message if (DEPOSIT_MSG) { setTimeout(function() { showAlert(DEPOSIT_OK?'βœ…':'❌', DEPOSIT_OK?'Deposit Successful':'Deposit Failed', DEPOSIT_MSG, DEPOSIT_OK?'ok':'err'); // If successful deposit and logged in, reload wallet if (DEPOSIT_OK && INITIAL_USER) { setTimeout(function() { getApi('/api/game_state', function(d) { if (d && d.success && d.wallet) { S.cash = d.wallet.cash; S.fuel = d.wallet.fuel; S.maxFuel = Math.max(S.maxFuel, S.fuel); updateWallet(); } }); }, 800); } }, 600); } function doLogin() { var phone=el('liPhone').value.trim(), pass=el('liPass').value; if(!phone||!pass){showErr('Enter your phone number and password');return;} var ph=phone.replace(/[\s\-\+]/g,''); if(!/^(07|01|254|7|1)[0-9]{8,9}$/.test(ph)){showErr('Enter a valid phone number (e.g. 0712 345 678)');return;} postApi('/api/auth?action=login',{phone:phone,password:pass},function(d){ if(d.success){ closeModal(); onLogin({username:d.username,cash:d.cash_balance,fuel:d.fuel_balance,refCode:d.referral_code||'',refCount:d.refCount||0,refFuel:d.refFuel||0}); } else showErr(d.error||'Login failed'); }); } function doRegister() { var name=el('rgName').value.trim(), email=el('rgEmail').value.trim(); var phone=el('rgPhone').value.trim(), pass=el('rgPass').value; if(!name||!phone||!pass){showErr('Username, phone number and password are required');return;} if(pass.length<8){showErr('Password must be at least 8 characters');return;} // Basic Kenyan phone validation var ph=phone.replace(/[\s\-\+]/g,''); if(!/^(07|01|254)[0-9]{8,9}$/.test(ph)){showErr('Enter a valid Kenyan phone number (e.g. 0712 345 678)');return;} postApi('/api/auth?action=register',{username:name,email:email,phone:phone,password:pass,ref:URL_REF},function(d){ if(d.success){ closeModal(); showAlert('πŸŽ‰','Welcome, '+name+'!','You\'ve received 2L free fuel. Start driving!','ok'); onLogin({username:d.username||name,cash:d.cash_balance||0,fuel:d.fuel_balance||2,refCode:d.referral_code||'',refCount:0,refFuel:0}); } else showErr(d.error||'Registration failed'); }); } function doLogout() { getApi('/api/auth?action=logout',function(){location.reload();}); } function doForgot() { var email=el('fgEmail').value.trim(); if(!email){showErr('Enter your email address');return;} postApi('/api/reset_password',{action:'request',email:email},function(d){ if(d&&d.success){ showSuc('Reset link sent! Check your email inbox (and spam folder).'); el('fgEmail').value=''; } else { showErr(d&&d.error?d.error:'Could not send reset email. Try again.'); } }); } // ════════════════════════════════════════════════════════ // PAYMENTS // ════════════════════════════════════════════════════════ function doJengaWithdraw() { var ph=el('wdJengaPhone').value.trim(), am=parseFloat(el('wdJengaKes').value); if(!ph||!am||am<100){showErr('Enter phone and amount (min KES 100)');return;} var phpn=ph.replace(/[\s\-\+]/g,''); if(!/^(07|01|254|7|1)[0-9]{8,9}$/.test(phpn)){showErr('Enter a valid Kenyan phone number');return;} postApi('/api/jenga_withdraw',{phone:ph,amount_kes:am},function(d){ if(d&&d.success){ closeModal(); showAlert('πŸ’Έ','Withdrawal Submitted',d.message,'ok'); getApi('/api/game_state',function(gs){ if(gs&&gs.wallet){S.cash=gs.wallet.cash;S.fuel=gs.wallet.fuel;updateWallet();} }); setTimeout(loadActivityFromDB, 500); } else showErr(d&&d.error?d.error:'Equity Jenga withdrawal failed.'); }); } function doJenga() { var ph=el('jengaPhone').value.trim(), am=parseFloat(el('jengaAmt').value); if(!ph||!am||am<10){showErr('Enter valid phone and amount (min KES 10)');return;} var phpn=ph.replace(/[\s\-\+]/g,''); if(!/^(07|01|254|7|1)[0-9]{8,9}$/.test(phpn)){showErr('Enter a valid Kenyan phone number');return;} postApi('/api/jenga',{phone:ph,amount:am},function(d){ if(d&&d.success){closeModal();showAlert('πŸ“±','M-Pesa PIN Required','STK Push sent via Equity Jenga! Enter your PIN on your phone.','info');} else showErr(d&&d.error?d.error:'Equity Jenga request failed. Check config.'); }); } function doMpesa() { var ph=el('mpPhone').value.trim(), am=parseFloat(el('mpAmt').value); if(!ph||!am||am<10){showErr('Enter valid phone and amount (min KES 10)');return;} postApi('/api/mpesa',{phone:ph,amount:am},function(d){ if(d&&d.success){closeModal();showAlert('πŸ“±','M-Pesa PIN Required','STK Push sent! Enter your M-Pesa PIN on your phone.','info');} else showErr(d&&d.error?d.error:'M-Pesa request failed. Check config/credentials.'); }); } function doPaypal() { var am=parseFloat(el('ppAmt').value); if(!am||am<130){showErr('Enter amount (min KES 130)');return;} postApi('/api/paypal',{amount:am},function(d){ if(d.success&&d.approve_url)window.location.href=d.approve_url; else showErr(d.error||'PayPal request failed. Check config/credentials.'); }); } function doWithdraw(method) { var body={method:method}; if(method==='mpesa'){ body.phone=el('wdPhone').value.trim(); body.amount_kes=parseFloat(el('wdKes').value); if(!body.phone||!body.amount_kes){showErr('Fill phone and amount');return;} if(body.amount_kes<100){showErr('Minimum withdrawal is KES 100');return;} } else { body.email=el('wdEmail').value.trim(); body.amount_kes=parseFloat(el('wdUsd').value); // wdUsd field now holds KES if(!body.email||!body.amount_kes){showErr('Fill email and amount');return;} if(body.amount_kes<650){showErr('Minimum PayPal withdrawal is KES 650');return;} } postApi('/api/withdraw',body,function(d){ if(d.success){ closeModal(); showAlert('πŸ’Έ','Withdrawal Submitted','Arrives in 1–3 minutes.','ok'); getApi('/api/game_state',function(gs){ if(gs&&gs.wallet){S.cash=gs.wallet.cash;S.fuel=gs.wallet.fuel;updateWallet();} }); setTimeout(loadActivityFromDB, 500); } else showErr(d.error||'Withdrawal failed'); }); } function updateFuelCost() { var l=parseFloat(el('fuelLtrs').value)||0; var cd=el('fuelCostDisplay'); if(cd)cd.textContent=kesF(l*FUEL_KES); } function doBuyFuel() { var ltrs=parseFloat(el('fuelLtrs').value); if(!ltrs||ltrs<1){showErr('Enter litres to buy (min 1L)');return;} var cost=ltrs*FUEL_KES; if(cost>S.cash){showErr('Insufficient cash. Deposit first. Need '+kesF(cost));return;} postApi('/api/buy_fuel',{litres:ltrs},function(d){ if(d.success){ S.cash=d.cash_balance; S.fuel=d.fuel_balance; S.maxFuel=Math.max(S.maxFuel,S.fuel); updateWallet(); closeModal(); showAlert('β›½','Fuel Added',ltrs+'L added to your tank!','ok'); setTimeout(loadActivityFromDB, 400); } else showErr(d.error||'Failed to buy fuel'); }); } // ── REFERRAL ───────────────────────────────────────────── function copyRef() { var link=window.location.origin+'/?ref='+S.refCode; if(navigator.clipboard){ navigator.clipboard.writeText(link).then(function(){showToast('πŸ“‹ Referral link copied!');}); } else { prompt('Copy this referral link:',link); } } // ════════════════════════════════════════════════════════ // MODAL SYSTEM // ════════════════════════════════════════════════════════ var PANES=['login','register','dep','wd','fuel','profile','howto','faq','forgot']; var TITLES={login:'LOG IN',register:'CREATE ACCOUNT',dep:'ADD FUNDS',wd:'WITHDRAW FUNDS',fuel:'BUY FUEL',profile:'MY PROFILE',howto:'HOW TO PLAY',faq:'FAQ',forgot:'RESET PASSWORD'}; function openModal(type) { if((type==='dep'||type==='wd'||type==='fuel')&&!S.loggedIn){openModal('login');return;} el('mbg').classList.add('mbg-on'); PANES.forEach(function(p){var e=el('mpane-'+p);if(e)e.className='mpane';}); clearMsg(); el('mttl').textContent=TITLES[type]||''; var pane=el('mpane-'+type); if(pane)pane.className='mpane mpane-on'; // Referral badge if(type==='register'&&URL_REF){ var rb=el('refBadge'); if(rb)rb.innerHTML='
🎁 Referral code '+URL_REF+' applied β€” you\'ll get bonus fuel!
'; } // Withdraw available if(type==='wd'){var wa=el('wdAvail');if(wa)wa.textContent=kesF(S.cash);} } function closeModal() { el('mbg').classList.remove('mbg-on'); } function showErr(msg) { var e=el('merr'); e.textContent=msg; e.className='merr merr-on'; el('msuc').className='msuc'; } function showSuc(msg) { var e=el('msuc'); e.textContent=msg; e.className='msuc msuc-on'; el('merr').className='merr'; } function clearMsg() { el('merr').className='merr'; el('msuc').className='msuc'; } function payTab(sec,method) { var prefix=sec+'-'; var depMethods = ['jenga','mpesa','paypal']; var wdMethods = ['jenga','mpesa','paypal']; var methods = sec==='dep' ? depMethods : wdMethods; methods.forEach(function(m){ var t=el(prefix+'tab-'+m); if(t)t.className='mtab'; var p=el(prefix+m); if(p)p.className='mpane'; }); var at=el(prefix+'tab-'+method); if(at)at.className='mtab mtab-on'; var ap=el(prefix+method); if(ap)ap.className='mpane mpane-on'; } var _alertTimer=null; function showAlert(icon,title,msg,type){ // type: 'ok'|'err'|'warn'|'info' var t=type||'info'; var box=el('caBox'); var al=el('customAlert'); if(!box||!al) return; el('caIcon').textContent=icon||'ℹ️'; el('caTitle').textContent=title||'Notice'; el('caMsg').textContent=msg||''; box.className='ca-box ca-'+t; al.classList.add('ca-on'); if(_alertTimer) clearTimeout(_alertTimer); _alertTimer=setTimeout(dismissAlert,5000); } function dismissAlert(){ var al=el('customAlert'); if(al)al.classList.remove('ca-on'); if(_alertTimer){clearTimeout(_alertTimer);_alertTimer=null;} } function showToast(msg, type) { var t=document.createElement('div'); t.className='toast'; t.textContent=msg; if(type==='err') t.style.borderColor='var(--rd)'; else if(type==='ok' || (msg&&msg.charAt(0)==='βœ…')) t.style.borderColor='var(--gr)'; document.body.appendChild(t); setTimeout(function(){t.style.opacity='0';setTimeout(function(){t.remove();},400);},3200); } // Enter key submits login/register document.addEventListener('keydown',function(e){ if(e.key!=='Enter') return; var m=el('mbg'); if(!m.classList.contains('mbg-on')) return; var lp=el('mpane-login'); if(lp&&lp.classList.contains('mpane-on')){doLogin();return;} var rp=el('mpane-register'); if(rp&&rp.classList.contains('mpane-on')){doRegister();} }); // ── TABS ───────────────────────────────────────────────── function sTab(id,clickedEl) { document.querySelectorAll('.stab').forEach(function(t){t.className='stab';}); document.querySelectorAll('.tp').forEach(function(p){p.className='tp';}); clickedEl.className='stab stab-on'; var tp=el('tp-'+id); if(tp)tp.className='tp tp-on'; } function mNav(id,clickedEl) { // Hide all mobile panels ['lead','chat','acct'].forEach(function(p){ var x=el('mp-'+p); if(x){x.classList.remove('mpanel-on');} }); // Update nav highlight document.querySelectorAll('.ni').forEach(function(n){n.classList.remove('ni-on');}); clickedEl.classList.add('ni-on'); // Show the requested panel if(id!=='game'){ var x=el('mp-'+id); if(x){ x.classList.add('mpanel-on'); if(id==='chat') syncChatToMobile(); } } } // ── LEADERBOARD ─────────────────────────────────────────── // 15 shuffled names that rotate after every game (mix real + synthetic) var LB_NAMES=[ 'Kamau_254','Wanjiku_G','OmondiFast','Kipchoge_K','NjeriDrift', 'MuthuaRace','AkinaNairobi','SigomoSpeed','WaweruTurbo','ChegeApex', 'MuthoniV8','KaranjaNFS','OtienoFuel','NgugiLap','AchiengGear', 'MwangiNRB','KaviaNight','OdingaRev','NjeruBurn','WambaMPH', 'MburuSlip','KilomebaK','NyamweaRed','OgolaNitro','WairimuWin']; var LB_EMOJIS=['🏎️','πŸš—','🏎️','πŸš—','🏎️','πŸš—','🏎️','πŸš—']; var lbCache=[]; function shuffleLeaderboard(realLeaders) { // Mix real DB leaders with generated names for 15 entries var items=[]; if(realLeaders&&realLeaders.length){ realLeaders.forEach(function(r){ items.push({name:r.username,amount:parseFloat(r.total_won),real:true,best:parseFloat(r.best_mult||0)}); }); } // Fill to 15 with synthetic random players var used=items.map(function(x){return x.name;}); var pool=LB_NAMES.filter(function(n){return used.indexOf(n)<0;}); // Shuffle pool for(var i=pool.length-1;i>0;i--){var j=~~(Math.random()*(i+1));var t=pool[i];pool[i]=pool[j];pool[j]=t;} while(items.length<15&&pool.length){ var n=pool.shift(); items.push({name:n,amount:Math.round(6500+Math.random()*104000),real:false,best:parseFloat((1.5+Math.random()*8).toFixed(2))}); } // Sort descending by amount items.sort(function(a,b){return b.amount-a.amount;}); // Assign slight random variation each call (makes it feel live) items.forEach(function(it){ if(!it.real)it.amount+=Math.round((Math.random()-0.5)*2340); }); lbCache=items; renderLeaderboard(); } function renderLeaderboard() { var ranks=['lbp-g','lbp-s','lbp-b','','','']; var emojis=LB_EMOJIS; var html=''; lbCache.forEach(function(it,i){ var rc=ranks[i]||''; var em=emojis[i%emojis.length]; html+='
'+ ''+(i+1)+''+ ''+em+' '+it.name+''+ ''+kesF(Math.max(0,it.amount))+''+ '
'; }); ['lbList','lbListM'].forEach(function(id){ var e=el(id); if(e)e.innerHTML=html; }); } function loadLeaderboard() { getApi('/api/leaderboard',function(d){ shuffleLeaderboard(d&&d.leaders?d.leaders:[]); }); } // Rotate leaderboard after each game round function refreshLeaderboard() { if(lbCache.length){ // Shuffle order slightly and re-render lbCache.forEach(function(it){ if(!it.real) it.amount+=Math.round((Math.random()-0.35)*22); }); lbCache.sort(function(a,b){return b.amount-a.amount;}); renderLeaderboard(); } else { loadLeaderboard(); } } // Initial load for guests too setTimeout(loadLeaderboard, 800); // Refresh leaderboard after every simulated crash round ends // We patch simCrash2 to call refreshLeaderboard after the delay var _origSimCrash2=simCrash2; simCrash2=function(){ _origSimCrash2(); // refreshLeaderboard is called after the round restarts (inside the setTimeout) }; // ── USER PROFILE ────────────────────────────────────────── function openProfile() { if(!S.loggedIn){openModal('login');return;} // show modal with profile pane el('mbg').classList.add('mbg-on'); PANES.forEach(function(p){var e=el('mpane-'+p);if(e)e.className='mpane';}); clearMsg(); el('mttl').textContent='MY PROFILE'; var pane=el('mpane-profile'); if(pane)pane.className='mpane mpane-on'; // Show loading, hide content var pl=el('profLoading'), pc=el('profContent'); if(pl)pl.style.display='block'; if(pc)pc.style.display='none'; // Fetch from API getApi('/api/profile',function(d){ if(!d||!d.success||!d.profile){ if(pl)pl.textContent='Failed to load profile.'; return; } var p=d.profile; if(pl)pl.style.display='none'; if(pc)pc.style.display='block'; function se(id,v){var e=el(id);if(e)e.textContent=v;} se('profName',p.username); se('profEmail',p.email||(p.username+'@endesha')); se('profSince','Member since '+new Date(p.member_since).toLocaleDateString('en-KE',{year:'numeric',month:'short'})); se('profCash',kesF(p.cash)); se('profFuel',parseFloat(p.fuel).toFixed(1)+'L'); se('profWins',p.wins); se('profBest',p.best_mult>0?p.best_mult+'x':'β€”'); se('profWon',kesF(p.total_won)); se('profBets',p.total_bets); se('profRefCode',p.referral_code); se('profRefCount',p.ref_count); se('profRefFuel',parseFloat(p.ref_fuel_earned).toFixed(1)+' L earned'); var rl=el('profRefLink'); if(rl)rl.textContent=window.location.origin+'/?ref='+p.referral_code; }); } function copyRefFromProfile() { var rl=el('profRefLink'); var link=rl?rl.textContent:(window.location.origin+'/?ref='+S.refCode); if(navigator.clipboard){ navigator.clipboard.writeText(link).then(function(){showToast('πŸ“‹ Referral link copied!');}); } else { prompt('Copy your referral link:',link); } } // ── MOBILE CHAT HELPERS ─────────────────────────────────── function syncChatToMobile() { var src = el('chatMsgs'), dst = el('chatMsgsM'); if(src && dst){ dst.innerHTML = src.innerHTML; dst.scrollTop = dst.scrollHeight; } } function toggleEmojiPickerM() { var p = el('emojiPickerM'); if(p) p.style.display = (p.style.display==='flex'?'none':'flex'); } function insertEmojiM(e) { var i = el('chatInputM'); if(i){ i.value += e; i.focus(); } var p = el('emojiPickerM'); if(p) p.style.display='none'; } function sendChatM() { var i = el('chatInputM'); if(!i) return; var msg = i.value.trim(); if(!msg) return; // Mirror value to the desktop input and send var di = el('chatInput'); if(di) di.value = msg; i.value = ''; sendChat(); // Re-sync after a short delay so the new message appears setTimeout(syncChatToMobile, 600); } // Keep mobile chat in sync whenever the desktop chat updates var _origLoadChat = typeof loadChat==='function' ? loadChat : null; // Patch via MutationObserver on chatMsgs (function(){ var src = el('chatMsgs'); if(!src) return; var obs = new MutationObserver(function(){ syncChatToMobile(); }); obs.observe(src, {childList:true, subtree:true}); })(); // ── EXPOSE GLOBALS ──────────────────────────────────────── window.openModal=openModal; window.closeModal=closeModal; window.doLogin=doLogin; window.doRegister=doRegister; window.doLogout=doLogout; window.doForgot=doForgot; window.doJenga=doJenga; window.doJengaWithdraw=doJengaWithdraw; window.doMpesa=doMpesa; window.doPaypal=doPaypal; window.doWithdraw=doWithdraw; window.doBuyFuel=doBuyFuel; window.updateFuelCost=updateFuelCost; window.copyRef=copyRef; window.toggleTurbo=toggleTurbo; window.doStake=doStake; window.doCO=doCO; window.sTab=sTab; window.mNav=mNav; window.payTab=payTab; window.toggleDash=toggleDash; window.hideFuelAlert=hideFuelAlert; window.openProfile=openProfile; window.copyRefFromProfile=copyRefFromProfile; window.loadHistoryFromDB=loadHistoryFromDB; window.loadActivityFromDB=loadActivityFromDB; window.sendChat=sendChat; window.loadChat=loadChat; window.toggleEmojiPicker=toggleEmojiPicker; window.insertEmoji=insertEmoji; window.sendChatM=sendChatM; window.toggleEmojiPickerM=toggleEmojiPickerM; window.insertEmojiM=insertEmojiM; window.showAlert=showAlert; window.dismissAlert=dismissAlert; })(); // end IIFE