CURLQUEST
STRENGTH0
CASH$0
Noodle Arms
next: 100
NO PAIN
NO GAIN
CURLS
GET THE
GIRLS
Dumbbell Curls
TAP TO CURL
πŸ—ΊοΈ GYM MAPtap a station to walk there
πŸͺ΄ πŸͺ΄ 🧴 πŸ””
Made with wish4 +fmt(d.cost), 'db:'+i, S.cash < d.cost)}`; }); h += `
πŸ›οΈ BENCH PRESS BAR
`; if (!S.unlocked.bench) h += `
πŸ”’
Bench Press lockedReach ${fmt(ST.bench.req)} strength to unlock the bench
`; else BENCH.forEach((b,i)=>{ const owned = i <= S.bench, next = i === S.bench + 1; h += `
β—‰
${b.n} Β· ${b.w}kg+${b.w*5} strength per rep${i===S.bench?' Β· LOADED':''}
${owned ? btn(i===S.bench?'LOADED':'OWNED','',true,true) : btn('$'+fmt(b.cost), 'bench:'+i, !next || S.cash < b.cost)}
`; }); h += `
πŸ₯Š FIGHT GEAR β€” hit walls harder
`; if (!S.unlocked.wall) h += `
πŸ”’
Wall Punch lockedReach ${fmt(ST.wall.req)} strength to unlock the walls
`; else GLOVES.forEach((g,i)=>{ const owned = i <= S.gloves, next = i === S.gloves + 1; h += `
✊
${g.n}Γ—${g.m} punch damage${i===S.gloves?' Β· WEARING':''}
${owned ? btn(i===S.gloves?'WEARING':'OWNED','',true,true) : btn('$'+fmt(g.cost), 'gloves:'+i, !next || S.cash < g.cost)}
`; }); const left = Math.max(0, Math.ceil((S.shakeUntil - Date.now())/1000)); h += `
⚑ BOOSTS
πŸ₯€
Protein Shake2Γ— all gains for 30 seconds${left?` Β· ${left}s left`:''}
${btn('$'+fmt(shakeCost()),'shake',S.cash
πŸ§‘β€πŸ€β€πŸ§‘
Gym Buddy (${S.buddies})Curls for you automatically Β· +${fmt(DB[S.db].w*.5*mult())}/s each
${btn('$'+fmt(buddyCost()),'buddy',S.cash
πŸ“Š YOUR STATS
πŸ†
${RANKS[rankIdx(S.str)].n} Β· Size ${sizePct()}%${fmt(S.reps)} total reps Β· Cardio Lv ${cardioLv()} Β· ${mult().toFixed(1)}Γ— gains Β· Walls smashed: ${S.wallIdx} Β· Dumbbells grabbed: ${S.grabbed||0}
`; $('shopBody').innerHTML = h; } $('shopBody').addEventListener('click', e=>{ const b = e.target.closest('[data-act]'); if (!b || b.disabled) return; const [act, val] = b.dataset.act.split(':'); const i = +val; if (act === 'db' && S.cash >= DB[i].cost) { S.cash -= DB[i].cost; S.db = i; drawDumbbell(); toast(`πŸ‹οΈ ${DB[i].n} equipped!`); beep(660,.15,'triangle',.08); } else if (act === 'bench' && S.cash >= BENCH[i].cost) { S.cash -= BENCH[i].cost; S.bench = i; drawBenchPlate(); toast(`πŸ›οΈ ${BENCH[i].n} loaded!`); beep(660,.15,'triangle',.08); } else if (act === 'gloves' && S.cash >= GLOVES[i].cost) { S.cash -= GLOVES[i].cost; S.gloves = i; drawGloves(); toast(`πŸ₯Š ${GLOVES[i].n} on!`); beep(660,.15,'triangle',.08); } else if (act === 'shake' && S.cash >= shakeCost()) { S.cash -= shakeCost(); S.shakeUntil = Math.max(Date.now(), S.shakeUntil) + 30000; toast('πŸ₯€ Protein Shake! 2Γ— gains'); beep(990,.15,'triangle',.08); } else if (act === 'buddy' && S.cash >= buddyCost()) { S.cash -= buddyCost(); S.buddies++; toast('πŸ§‘β€πŸ€β€πŸ§‘ A gym buddy joined you!'); beep(740,.15,'triangle',.08); } else if (act === 'reset') { if (!confirm('Reset all progress? Your gains will be gone forever.')) return; clearDrop(); nextDropAt = Date.now() + 3500; S = fresh(); speed = 0; benchP = 0; lastRank = 0; lastSizeStep = Math.floor(sizePct()/5); init(); closeShop(); toast('Fresh start. Time to curl!'); return; } save(); renderShop(); updateHud(); }); /* ---------- HUD ---------- */ function updateHud(){ $('sStr').textContent = fmt(S.str); $('sCash').textContent = '$' + fmt(S.cash); const r = rankIdx(S.str), cur = RANKS[r], nxt = RANKS[r+1]; $('rankName').textContent = cur.n; if (nxt) { $('rankFill').style.width = Math.min(100, (S.str-cur.t)/(nxt.t-cur.t)*100) + '%'; $('rankNext').textContent = 'next: ' + fmt(nxt.t); } else { $('rankFill').style.width = '100%'; $('rankNext').textContent = 'MAX RANK'; } let b = `πŸ“ Size ${sizePct()}%`; if (myPos) b += `πŸ† #${myPos} on board`; if (shakeOn()) b += `πŸ₯€ 2Γ— Β· ${Math.ceil((S.shakeUntil-Date.now())/1000)}s`; if (cardioLv() > 0) b += `❀️ Cardio Lv ${cardioLv()} (+${cardioLv()*10}%)`; if (S.buddies) b += `πŸ§‘β€πŸ€β€πŸ§‘ +${fmt(buddyRate())}/s`; const nextLock = UNLOCKS.find(k=>!S.unlocked[k]); if (nextLock) b += `πŸ”’ ${ST[nextLock].label} at ${fmt(ST[nextLock].req)}`; const bd = $('badges'); if (bd.innerHTML !== b) bd.innerHTML = b; let info = ''; if (S.station === 'curl') info = `${DB[S.db].n} ${DB[S.db].w}kg Β· +${fmt(Math.max(1,DB[S.db].w*mult()))} per curl Β· every curl = bigger body`; else if (S.station === 'run') info = `Speed ${speed.toFixed(1)} Β· running builds cardio = bigger gains everywhere`; else if (S.station === 'wall') info = `${wallName(S.wallIdx)} wall Β· ${fmt(Math.max(0, wallHp(S.wallIdx) - S.wallDmg))} HP Β· -${fmt(punchDmg())} per punch`; else info = `${BENCH[S.bench].n} ${BENCH[S.bench].w}kg Β· +${fmt(BENCH[S.bench].w*5*mult())} per full rep`; const si = $('stInfo'); if (si.innerHTML !== info) si.innerHTML = info; } /* ---------- loop ---------- */ let last = performance.now(), lastDur = 0, saveAcc = 0; function loop(now){ const dt = Math.min(.1, (now - last)/1000); last = now; speed = Math.max(0, speed - 2*dt); if (S.station === 'run' && speed > 0) { S.cardioXp += speed*dt; const m = mult(); S.str += speed*dt*0.25*m; S.cash += speed*dt*0.35*m*(1+S.db*.5); checkProgress(); } const run = $('scRun'); if (speed > 0.15) { const dur = Math.max(.16, Math.round((0.75 - speed*0.05)*20)/20); if (dur !== lastDur) { run.style.setProperty('--dur', dur+'s'); run.style.setProperty('--bdur', (dur*0.7)+'s'); lastDur = dur; } run.style.setProperty('--play', 'running'); } else run.style.setProperty('--play', 'paused'); $('tmTxt').textContent = speed.toFixed(1); benchP = Math.max(0, benchP - 0.32*dt); if (S.station === 'bench') { const barY = 172 - benchP*70, sx = 262, sy = 192, hx = 262, hy = barY; const d = Math.hypot(sx-hx, sy-hy), seg = 46; const bend = d < seg*2 ? Math.sqrt(seg*seg - (d/2)*(d/2)) : 0; $('benchArm').setAttribute('points', `${sx},${sy} ${(sx+hx)/2 - bend},${(sy+hy)/2} ${hx},${hy}`); $('barbell').setAttribute('transform', `translate(${hx},${hy})`); $('benchMeter').setAttribute('height', benchP*60); $('benchMeter').setAttribute('y', 100 - benchP*60); } if (S.buddies) { const a = buddyRate()*dt; S.str += a; S.cash += a; checkProgress(); } if (!dropEl && Date.now() > nextDropAt) { spawnDrop(); if (!dropEl) nextDropAt = Date.now() + 1500; } updateHud(); saveAcc += dt; if (saveAcc > 2) { saveAcc = 0; save(); } lbAcc += dt; if (lbAcc > 20) { lbAcc = 0; if (LB.name) syncLB().then(fetchBoard).catch(()=>{}); } requestAnimationFrame(loop); } /* ---------- online leaderboard ---------- */ const LB_API = "https://wish4.ai/api/projects/592/data/Lifter"; let LB = {}; try { LB = JSON.parse(localStorage.getItem('curlquest_lb')) || {}; } catch(e){} const saveLB = () => { try{ localStorage.setItem('curlquest_lb', JSON.stringify(LB)); }catch(e){} }; const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); let myPos = 0, lbAcc = 0, lastSent = -1, syncing = null, renaming = false; const lbPayload = () => ({ name: LB.name, strength: Math.floor(S.str), rank: RANKS[rankIdx(S.str)].n, size: sizePct(), reps: Math.floor(S.reps), walls: S.wallIdx }); function syncLB(force){ if (!LB.name) return Promise.resolve(); if (syncing) return syncing; const p = lbPayload(); if (!force && p.strength === lastSent) return Promise.resolve(); syncing = (async () => { try { let res = null; if (LB.id) { res = await fetch(`${LB_API}/${LB.id}`, { method:'PATCH', headers:{'Content-Type':'application/json'}, body: JSON.stringify(p) }); if (res.status === 404) { LB.id = null; saveLB(); } } if (!LB.id) { res = await fetch(LB_API, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(p) }); if (res.ok) { const r = await res.json(); LB.id = r.id != null ? r.id : (r.record && r.record.id); saveLB(); } } if (res && res.ok) lastSent = p.strength; } catch(e) {} finally { syncing = null; } })(); return syncing; } async function fetchBoard(){ const res = await fetch(`${LB_API}?sort=strength&order=desc&limit=100`); if (!res.ok) throw new Error('load failed'); const data = await res.json(); const records = (data.records || []).slice().sort((a,b)=>(b.strength||0)-(a.strength||0)); const i = LB.id != null ? records.findIndex(r => String(r.id) === String(LB.id)) : -1; myPos = i >= 0 ? i + 1 : 0; return records; } const medal = i => i === 0 ? 'πŸ₯‡' : i === 1 ? 'πŸ₯ˆ' : i === 2 ? 'πŸ₯‰' : '#' + (i + 1); function lbRow(r, i){ const me = LB.id != null && String(r.id) === String(LB.id); return `
${i < 0 ? '100+' : medal(i)} ${esc(r.name)}${me ? ' (you)' : ''}${esc(r.rank || 'Noodle Arms')} Β· Size ${Math.round(r.size || 100)}% Β· ${fmt(r.walls || 0)} walls ${fmt(r.strength || 0)}STRENGTH
`; } function renderMe(){ const box = $('lbMe'); if (!LB.name || renaming) { box.innerHTML = `
Your strength, rank and size update automatically while you train.
`; } else { box.innerHTML = `
Lifting as ${esc(LB.name)}${myPos ? ` Β· you're #${myPos}` : ''}
`; } $('lbBtn').classList.toggle('new', !LB.name); } function renderList(records){ if (!records.length) { $('lbList').innerHTML = `

No lifters yet. Be the first on the board!

`; return; } let h = records.slice(0, 50).map(lbRow).join(''); if (LB.id != null && (!myPos || myPos > 50)) h += `
β€’ β€’ β€’
` + lbRow(Object.assign(lbPayload(), { id: LB.id }), myPos ? myPos - 1 : -1); $('lbList').innerHTML = h; } async function loadLB(){ renderMe(); $('lbList').innerHTML = `

Loading the strongest lifters…

`; try { await syncLB(true); const recs = await fetchBoard(); renderMe(); renderList(recs); } catch(e){ $('lbList').innerHTML = `

Couldn't load the leaderboard. Check your connection and tap ↻.

`; } } function openLB(){ if ($('shop').classList.contains('open')) closeShop(); renaming = false; $('lb').classList.add('open'); loadLB(); $('closeLb').focus(); } function closeLB(){ $('lb').classList.remove('open'); scene.focus(); } $('lbBtn').addEventListener('click', openLB); $('closeLb').addEventListener('click', closeLB); $('lbRefresh').addEventListener('click', loadLB); $('lb').addEventListener('click', e=>{ if (e.target.id === 'lb') closeLB(); }); $('lbMe').addEventListener('click', e=>{ if (e.target.id === 'lbRename') { renaming = true; renderMe(); $('lbName').focus(); } }); $('lbMe').addEventListener('submit', async e=>{ e.preventDefault(); e.stopPropagation(); const v = $('lbName').value.replace(/\s+/g, ' ').trim().slice(0, 16); if (v.length < 2) { toast('Your name needs at least 2 letters', true); return; } const first = !LB.name; LB.name = v; renaming = false; saveLB(); toast(first ? `πŸ† ${v} joined the leaderboard!` : `Name changed to ${v}`); beep(784,.15,'triangle',.08); await loadLB(); }); renderMe(); if (LB.name) syncLB(true).then(fetchBoard).catch(()=>{}); $('muteBtn').addEventListener('click', ()=>{ S.mute = !S.mute; $('muteBtn').textContent = S.mute ? 'πŸ”‡' : 'πŸ”Š'; save(); }); window.addEventListener('beforeunload', save); document.addEventListener('visibilitychange', ()=>{ if (document.hidden) { save(); syncLB(); } }); function init(){ drawDumbbell(); drawBenchPlate(); drawWall(false); drawGloves(); applyBody(); UNLOCKS.forEach(k=>{ if (S.str >= ST[k].req) S.unlocked[k] = true; }); if (!S.unlocked[S.station]) S.station = 'curl'; $('muteBtn').textContent = S.mute ? 'πŸ”‡' : 'πŸ”Š'; placePlayer(S.station); showScene(); refreshMap(); updateHud(); } init(); requestAnimationFrame(loop); })();