// ───── WEEKLY CHALLENGES PAGE (/challenges) ───── // Data from GET /api/site/weekly (WeeklyChallengeController). Signed in, every challenge carries // the player's own progress; signed out, the page shows the challenges and asks them to log in. (function () { const list = document.getElementById('wcList'); if (!list) return; function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } // Store rarity -> the name the game shows on its boxes. function rarityName(r) { if (r >= 50) return ['Legendary', 'legendary']; if (r >= 30) return ['Epic', 'epic']; if (r >= 20) return ['Rare', 'rare']; if (r >= 10) return ['Uncommon', 'uncommon']; return ['Common', 'common']; } function countdown(iso) { const ms = new Date(iso) - Date.now(); if (!(ms > 0)) return 'any moment'; const d = Math.floor(ms / 86400000), h = Math.floor(ms % 86400000 / 3600000), m = Math.floor(ms % 3600000 / 60000); return d > 0 ? d + 'd ' + h + 'h' : h > 0 ? h + 'h ' + m + 'm' : m + 'm'; } async function load() { let d = null; try { const r = await fetch('/api/site/weekly', { credentials: 'same-origin' }); if (r.ok) d = await r.json(); } catch { } if (!d || !d.ok) { list.innerHTML = '
Couldn\'t load this week\'s challenges. Try again in a moment.
'; return; } document.getElementById('wcNeed').textContent = d.required; // ---- challenges ---- list.innerHTML = d.challenges.map(c => { const n = Math.min(c.progress, c.target); const done = d.signedIn && n >= c.target; const pct = d.signedIn ? Math.round(100 * n / c.target) : 0; const room = c.roomId ? '^' + esc(c.roomName || 'Room') + '' : 'Any room'; return '
' + '
' + esc(c.text) + '
' + (d.signedIn ? '' + (done ? 'Done' : n + ' / ' + c.target) + '' : '') + '
' + '
' + room + '
' + (d.signedIn ? '
' : '') + '
'; }).join(''); // ---- the progress ring in the header ---- const need = d.required, have = d.signedIn ? Math.min(d.completed, need) : 0; const fill = document.getElementById('wcMeterFill'); const C = 2 * Math.PI * 52; fill.style.strokeDasharray = C; fill.style.strokeDashoffset = C * (1 - have / need); document.getElementById('wcMeterNum').textContent = d.signedIn ? have : '?'; document.getElementById('wcMeterOf').textContent = 'of ' + need; document.getElementById('wcMeter').setAttribute('aria-label', d.signedIn ? have + ' of ' + need + ' challenges done' : 'Log in to see your progress'); document.getElementById('wcMeter').classList.toggle('wc-meter-full', d.signedIn && (d.rewardSent || have >= need)); // ---- reward ---- const img = document.getElementById('wcRewardImg'); const r = d.reward; if (r) { if (r.image) { img.onerror = () => { img.hidden = true; }; img.src = r.image + '?width=360'; img.alt = r.name; img.hidden = false; } document.getElementById('wcRewardName').textContent = r.name; document.getElementById('wcRewardKind').textContent = r.kind === 'skin' ? 'Equipment skin' : r.kind === 'clothing' ? 'Clothing' : ''; const [rn, rc] = rarityName(r.rarity || 0); const badge = document.getElementById('wcRarity'); badge.textContent = rn; badge.hidden = false; document.getElementById('wcReward').classList.add('wc-r-' + rc); } else { document.getElementById('wcRewardName').textContent = 'A surprise'; } const st = document.getElementById('wcStatus'); if (!d.signedIn) { st.innerHTML = 'Log in to track your progress'; } else if (d.rewardSent) { st.className = 'wc-reward-status wc-earned'; st.textContent = 'Earned! ' + (d.rewardSentName && r && d.rewardSentName !== r.name ? 'You already had it, so you got a ' + d.rewardSentName + ' instead. ' : '') + 'Open your gifts in game.'; } else { const left = Math.max(0, d.required - d.completed); st.textContent = left > 0 ? left + ' more challenge' + (left === 1 ? '' : 's') + ' to unlock' : 'Unlocked - check your gifts in game'; } const timer = document.getElementById('wcTimer'); const tick = () => { timer.textContent = countdown(d.endsAt); }; tick(); setInterval(tick, 30000); } load(); })();