VALSERIS

Multiplayer Platform for Creators

oundEngine(); // 3. APPLICATION STATE const state = { user: null, profile: null, vals: [], myValIds: new Set(), equippedVal: null, friends: [], authMode: 'login' }; // 4. DOM ELEMENTS const authModal = document.getElementById('authModal'); const authForm = document.getElementById('authForm'); const appEl = document.getElementById('app'); const adminModal = document.getElementById('adminModal'); // 5. AUTHENTICATION CONTROLLER document.getElementById('tabLogin').addEventListener('click', () => setAuthTab('login')); document.getElementById('tabRegister').addEventListener('click', () => setAuthTab('register')); function setAuthTab(mode) { sound.playClick(); state.authMode = mode; const uGroup = document.getElementById('usernameGroup'); const submitBtn = document.getElementById('authSubmitBtn'); const tabL = document.getElementById('tabLogin'); const tabR = document.getElementById('tabRegister'); if (mode === 'register') { uGroup.classList.remove('hidden'); submitBtn.textContent = 'CREATE ACCOUNT'; tabR.className = 'flex-1 py-2 font-bold border-b-2 border-brand-accent text-brand-accent'; tabL.className = 'flex-1 py-2 font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200'; } else { uGroup.classList.add('hidden'); submitBtn.textContent = 'LOG IN'; tabL.className = 'flex-1 py-2 font-bold border-b-2 border-brand-accent text-brand-accent'; tabR.className = 'flex-1 py-2 font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200'; } } authForm.addEventListener('submit', async (e) => { e.preventDefault(); sound.playClick(); const email = document.getElementById('authEmail').value; const password = document.getElementById('authPassword').value; const username = document.getElementById('authUsername').value; const errEl = document.getElementById('authError'); errEl.classList.add('hidden'); try { if (state.authMode === 'register') { const { data, error } = await supabase.auth.signUp({ email, password, options: { data: { username } } }); if (error) throw error; alert('Account created! Logging in...'); } else { const { data, error } = await supabase.auth.signInWithPassword({ email, password }); if (error) throw error; } initApp(); } catch (err) { sound.playError(); errEl.textContent = err.message; errEl.classList.remove('hidden'); } }); document.getElementById('logoutBtn').addEventListener('click', async () => { await supabase.auth.signOut(); window.location.reload(); }); // 6. APPLICATION BOOTSTRAP & SYNC async function initApp() { const { data: { session } } = await supabase.auth.getSession(); if (!session) { authModal.classList.remove('hidden'); appEl.classList.add('hidden'); return; } state.user = session.user; authModal.classList.add('hidden'); appEl.classList.remove('hidden'); // Admin Check if (state.user.email === ADMIN_EMAIL) { document.getElementById('adminIndicator').classList.remove('hidden'); } await loadCatalog(); await syncProfile(); await loadFriends(); setupRealtimeChat(); setupAdminListeners(); } async function loadCatalog() { const { data } = await supabase.from('vals').select('*').order('price', { ascending: true }); state.vals = data || []; renderMarketAndCodex(); } async function syncProfile() { const { data: profile } = await supabase.from('profiles').select('*').eq('id', state.user.id).single(); if (!profile) return; state.profile = profile; // Inventory fetch const { data: inv } = await supabase.from('user_vals').select('val_id').eq('user_id', state.user.id); state.myValIds = new Set((inv || []).map(item => item.val_id)); // Equipped Val Object state.equippedVal = state.vals.find(v => v.id === profile.equipped_val_id) || { image_url: `https://api.dicebear.com/7.x/bottts/svg?seed=${profile.username}` }; // Header Updates document.getElementById('navUsername').textContent = profile.username; document.getElementById('navUserAvatar').src = state.equippedVal.image_url; document.getElementById('headerTokens').textContent = profile.tokens.toLocaleString(); document.getElementById('dashTokens').textContent = profile.tokens.toLocaleString(); document.getElementById('dashValsCount').textContent = `${state.myValIds.size} / ${state.vals.length}`; document.getElementById('headerLevel').textContent = Math.floor(profile.tokens / 200) + 1; renderInventory(); } // 7. NAVIGATION CONTROLLER document.querySelectorAll('.nav-btn').forEach(btn => { btn.addEventListener('click', () => { sound.playClick(); const target = btn.getAttribute('data-nav'); document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('bg-brand-cardHover', 'text-white')); btn.classList.add('bg-brand-cardHover', 'text-white'); document.querySelectorAll('.view-panel').forEach(p => p.classList.add('hidden')); document.getElementById(`view-${target}`).classList.remove('hidden'); document.getElementById('pageTitle').textContent = target.toUpperCase(); }); }); // 8. RENDERERS (MARKET, CODEX, INVENTORY) function renderMarketAndCodex() { const marketGrid = document.getElementById('marketGrid'); const codexGrid = document.getElementById('codexGrid'); marketGrid.innerHTML = ''; codexGrid.innerHTML = ''; state.vals.forEach(val => { const isOwned = state.myValIds.has(val.id); // Market Card const mCard = document.createElement('div'); mCard.className = `bg-brand-card border-2 border-brand-border rounded-2xl p-4 flex flex-col items-center text-center relative overflow-hidden rarity-${val.rarity}`; mCard.innerHTML = ` ${val.rarity}

${val.name}

${val.price}
`; marketGrid.appendChild(mCard); // Codex Card const cCard = mCard.cloneNode(true); cCard.querySelector('button').remove(); codexGrid.appendChild(cCard); }); document.querySelectorAll('.buy-btn').forEach(b => { b.addEventListener('click', async () => { const valId = b.getAttribute('data-id'); if (state.myValIds.has(valId)) return; sound.playClick(); const { data, error } = await supabase.rpc('buy_val', { p_user_id: state.user.id, p_val_id: valId }); if (error || !data.success) { sound.playError(); alert(data?.message || 'Purchase failed'); } else { sound.playCatch(); await syncProfile(); renderMarketAndCodex(); } }); }); } function renderInventory() { const invGrid = document.getElementById('inventoryGrid'); invGrid.innerHTML = ''; const owned = state.vals.filter(v => state.myValIds.has(v.id)); if (owned.length === 0) { invGrid.innerHTML = '
No Vals in inventory yet! Visit the Market.
'; return; } owned.forEach(val => { const isEquipped = state.profile.equipped_val_id === val.id; const card = document.createElement('div'); card.className = `bg-brand-card border-2 border-brand-border rounded-2xl p-4 flex flex-col items-center text-center rarity-${val.rarity}`; card.innerHTML = ` ${val.rarity}

${val.name}

`; invGrid.appendChild(card); }); document.querySelectorAll('.equip-btn').forEach(b => { b.addEventListener('click', async () => { const valId = b.getAttribute('data-id'); sound.playClick(); await supabase.from('profiles').update({ equipped_val_id: valId }).eq('id', state.user.id); await syncProfile(); }); }); } // 9. DAILY WHEEL CANVAS ENGINE const wheelModal = document.getElementById('wheelModal'); const wheelCanvas = document.getElementById('wheelCanvas'); const wheelCtx = wheelCanvas.getContext('2d'); const spinBtn = document.getElementById('spinBtn'); const slices = [50, 25, 100, 500, 250, 75]; const colors = ['#00f2fe', '#7928ca', '#ff0080', '#eab308', '#10b981', '#3b82f6']; let currentAngle = 0; let isSpinning = false; document.getElementById('openWheelBtn').addEventListener('click', () => { sound.playClick(); wheelModal.classList.remove('hidden'); drawWheel(); }); document.getElementById('closeWheelBtn').addEventListener('click', () => { sound.playClick(); wheelModal.classList.add('hidden'); }); function drawWheel() { const numSlices = slices.length; const sliceAngle = (2 * Math.PI) / numSlices; wheelCtx.clearRect(0, 0, 300, 300); wheelCtx.save(); wheelCtx.translate(150, 150); wheelCtx.rotate(currentAngle); for (let i = 0; i < numSlices; i++) { const start = i * sliceAngle; const end = start + sliceAngle; wheelCtx.beginPath(); wheelCtx.moveTo(0, 0); wheelCtx.arc(0, 0, 140, start, end); wheelCtx.fillStyle = colors[i]; wheelCtx.fill(); wheelCtx.stroke(); // Label wheelCtx.save(); wheelCtx.rotate(start + sliceAngle / 2); wheelCtx.textAlign = 'right'; wheelCtx.fillStyle = '#000000'; wheelCtx.font = '900 16px sans-serif'; wheelCtx.fillText(slices[i].toString(), 120, 5); wheelCtx.restore(); } wheelCtx.restore(); } spinBtn.addEventListener('click', async () => { if (isSpinning) return; isSpinning = true; spinBtn.disabled = true; // Select winning slice const winIndex = Math.floor(Math.random() * slices.length); const prize = slices[winIndex]; const sliceAngle = (2 * Math.PI) / slices.length; const targetRotation = (2 * Math.PI * 5) + ((slices.length - winIndex - 0.5) * sliceAngle); let startTime = null; const duration = 4000; function animateSpin(timestamp) { if (!startTime) startTime = timestamp; const elapsed = timestamp - startTime; const progress = Math.min(elapsed / duration, 1); const easeOut = 1 - Math.pow(1 - progress, 3); // Cubic ease out currentAngle = easeOut * targetRotation; drawWheel(); sound.playTick(); if (progress < 1) { requestAnimationFrame(animateSpin); } else { isSpinning = false; spinBtn.disabled = false; claimWheelPrize(prize); } } requestAnimationFrame(animateSpin); }); async function claimWheelPrize(amount) { const { data, error } = await supabase.rpc('claim_daily_tokens', { p_user_id: state.user.id, p_amount: amount }); if (error || !data.success) { sound.playError(); alert(data?.message || 'Daily cooldown active!'); } else { sound.playCatch(); alert(`🎉 You won ${amount} Tokens!`); await syncProfile(); } } // 10. FISHING MINI-GAME ENGINE const fishModal = document.getElementById('fishModal'); const fishCanvas = document.getElementById('fishCanvas'); const fishCtx = fishCanvas.getContext('2d'); const fishStatus = document.getElementById('fishStatus'); let fishState = 'IDLE'; // IDLE, WAITING, READY, REEL let fishTimer = null; let bobberY = 220; document.getElementById('openFishBtn').addEventListener('click', () => { sound.playClick(); fishModal.classList.remove('hidden'); resetFishGame(); }); document.getElementById('closeFishBtn').addEventListener('click', () => { sound.playClick(); fishModal.classList.add('hidden'); }); function resetFishGame() { fishState = 'IDLE'; fishStatus.textContent = 'Click Canvas to Cast'; drawFishScene(); } fishCanvas.addEventListener('click', () => { sound.playClick(); if (fishState === 'IDLE') { fishState = 'WAITING'; fishStatus.textContent = 'Waiting for bite...'; drawFishScene(); const delay = 2000 + Math.random() * 2000; fishTimer = setTimeout(() => { fishState = 'READY'; fishStatus.textContent = 'HOOKED! CLICK NOW!'; sound.playCatch(); drawFishScene(); fishTimer = setTimeout(() => { if (fishState === 'READY') { fishState = 'IDLE'; fishStatus.textContent = 'Fish Got Away! Click to retry.'; sound.playError(); drawFishScene(); } }, 1200); }, delay); } else if (fishState === 'READY') { clearTimeout(fishTimer); fishState = 'REEL'; catchFishReward(); } }); async function catchFishReward() { // Weighted Random Rewards const rand = Math.random() * 100; let prize = 30; if (rand < 3) prize = 300; // Legendary 3% else if (rand < 12) prize = 150; // Epic 9% else if (rand < 30) prize = 85; // Rare 18% else if (rand < 60) prize = 50; // Uncommon 30% fishStatus.textContent = `Caught Chest! +${prize} Tokens`; sound.playCatch(); await supabase.rpc('process_fish_catch', { p_user_id: state.user.id, p_amount: prize }); await syncProfile(); drawFishScene(); setTimeout(() => resetFishGame(), 2000); } function drawFishScene() { fishCtx.clearRect(0, 0, 600, 350); // Sky & Water Background fishCtx.fillStyle = '#0f172a'; fishCtx.fillRect(0, 0, 600, 200); fishCtx.fillStyle = '#0284c7'; fishCtx.fillRect(0, 200, 600, 150); // Wooden Dock fishCtx.fillStyle = '#78350f'; fishCtx.fillRect(0, 170, 180, 30); fishCtx.fillRect(40, 200, 20, 150); // Draw Val Player Sprite if (state.equippedVal) { const img = new Image(); img.src = state.equippedVal.image_url; fishCtx.drawImage(img, 100, 110, 60, 60); } // Fishing Line & Bobber if (fishState !== 'IDLE') { fishCtx.strokeStyle = '#ffffff'; fishCtx.beginPath(); fishCtx.moveTo(150, 130); fishCtx.lineTo(400, fishState === 'READY' ? 240 : 220); fishCtx.stroke(); // Bobber fishCtx.fillStyle = fishState === 'READY' ? '#ef4444' : '#ffffff'; fishCtx.beginPath(); fishCtx.arc(400, fishState === 'READY' ? 240 : 220, 8, 0, Math.PI * 2); fishCtx.fill(); } } // 11. REALTIME CHAT ENGINE const chatStream = document.getElementById('chatStream'); const chatForm = document.getElementById('chatForm'); async function setupRealtimeChat() { // Fetch initial 20 messages const { data: msgs } = await supabase .from('chat_messages') .select('*, profiles(username, equipped_val_id)') .order('created_at', { ascending: true }) .limit(30); if (msgs) { chatStream.innerHTML = ''; msgs.forEach(appendChatMessage); document.getElementById('dashMsgCount').textContent = msgs.length; } // Supabase Channel Subscription supabase .channel('global-chat') .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'chat_messages' }, async (payload) => { const { data: fullMsg } = await supabase .from('chat_messages') .select('*, profiles(username, equipped_val_id)') .eq('id', payload.new.id) .single(); if (fullMsg) { appendChatMessage(fullMsg); sound.playTick(); } }) .subscribe(); } function appendChatMessage(msg) { const val = state.vals.find(v => v.id === msg.profiles?.equipped_val_id); const avatarUrl = val ? val.image_url : `https://api.dicebear.com/7.x/bottts/svg?seed=${msg.profiles?.username || 'User'}`; const el = document.createElement('div'); el.className = 'flex items-start gap-3 bg-brand-bg/60 p-3 rounded-xl border border-brand-border/40'; el.innerHTML = `
${msg.profiles?.username || 'Player'} ${new Date(msg.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}

${msg.message}

`; chatStream.appendChild(el); chatStream.scrollTop = chatStream.scrollHeight; } chatForm.addEventListener('submit', async (e) => { e.preventDefault(); const input = document.getElementById('chatInput'); const text = input.value.trim(); if (!text) return; input.value = ''; await supabase.from('chat_messages').insert({ user_id: state.user.id, message: text }); }); // 12. FRIENDS CONTROLLER async function loadFriends() { const { data } = await supabase .from('friends') .select('*, requester:profiles!requester_id(username), receiver:profiles!receiver_id(username)') .or(`requester_id.eq.${state.user.id},receiver_id.eq.${state.user.id}`); const pendingList = document.getElementById('pendingFriendsList'); const acceptedList = document.getElementById('acceptedFriendsList'); pendingList.innerHTML = ''; acceptedList.innerHTML = ''; (data || []).forEach(f => { const isRequester = f.requester_id === state.user.id; const otherUsername = isRequester ? f.receiver?.username : f.requester?.username; if (f.status === 'pending') { const item = document.createElement('div'); item.className = 'flex items-center justify-between text-xs bg-brand-card p-2 rounded-lg'; item.innerHTML = ` ${otherUsername} ${!isRequester ? `` : 'Sent'} `; pendingList.appendChild(item); } else { const item = document.createElement('div'); item.className = 'text-xs bg-brand-card p-2 rounded-lg font-bold text-slate-200 flex items-center gap-2'; item.innerHTML = ` ${otherUsername}`; acceptedList.appendChild(item); } }); document.querySelectorAll('.accept-friend-btn').forEach(b => { b.addEventListener('click', async () => { await supabase.from('friends').update({ status: 'accepted' }).eq('id', b.getAttribute('data-id')); await loadFriends(); }); }); } document.getElementById('sendFriendReqBtn').addEventListener('click', async () => { const targetUser = document.getElementById('addFriendInput').value.trim(); if (!targetUser) return; const { data: targetProfile } = await supabase.from('profiles').select('id').eq('username', targetUser).single(); if (!targetProfile) { alert('User not found!'); return; } await supabase.from('friends').insert({ requester_id: state.user.id, receiver_id: targetProfile.id }); alert('Friend request sent!'); document.getElementById('addFriendInput').value = ''; await loadFriends(); }); // 13. OWNER ADMIN PANEL (CTRL + K) function setupAdminListeners() { window.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); if (state.user?.email === ADMIN_EMAIL) { sound.playClick(); adminModal.classList.remove('hidden'); } } }); document.getElementById('closeAdminBtn').addEventListener('click', () => adminModal.classList.add('hidden')); // Add Val document.getElementById('addValForm').addEventListener('submit', async (e) => { e.preventDefault(); const name = document.getElementById('adminValName').value; const image_url = document.getElementById('adminValUrl').value; const rarity = document.getElementById('adminValRarity').value; const price = parseInt(document.getElementById('adminValPrice').value); const { error } = await supabase.from('vals').insert({ name, image_url, rarity, price }); if (!error) { alert('Val published!'); adminModal.classList.add('hidden'); loadCatalog(); } }); // Update Tokens document.getElementById('adminTokensForm').addEventListener('submit', async (e) => { e.preventDefault(); const username = document.getElementById('adminTargetUsername').value; const tokens = parseInt(document.getElementById('adminSetTokens').value); const { data, error } = await supabase.rpc('admin_set_tokens', { p_target_username: username, p_amount: tokens }); if (data?.success) { alert(`Tokens updated for ${username}!`); adminModal.classList.add('hidden'); syncProfile(); } else { alert(data?.message || 'Error updating user tokens'); } }); } // STARTUP initApp();