diff --git a/frontend-backup/plugins/.gitignore b/frontend-backup/plugins/.gitignore deleted file mode 100644 index 181b3f1..0000000 --- a/frontend-backup/plugins/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Ignore all plugin files except the example -* -!.gitignore -!README.md -!example-button.js -|login-captcha.js -|pixel-lock.js \ No newline at end of file diff --git a/frontend-backup/plugins/example-info-modal.js b/frontend-backup/plugins/example-info-modal.js new file mode 100644 index 0000000..e2cc6bb --- /dev/null +++ b/frontend-backup/plugins/example-info-modal.js @@ -0,0 +1,266 @@ +/** + * Example FurryPlace Plugin - Info Modal Customization + * + * This demonstrates how to add custom sections to the info modal + * (the one that shows rules, YouTube video, etc.) + * + * To use this plugin, add it to your HTML: + * + */ + +(function() { + 'use strict'; + + // Set this to true to disable this example plugin (for reference only) + const EXAMPLE_DISABLED = true; + + // Wait for SDK to be available + function waitForSDK(callback) { + if (window.FurryPlaceSDK) { + callback(); + } else { + setTimeout(() => waitForSDK(callback), 100); + } + } + + // Example 1: Simple text section + function addWelcomeSection() { + window.FurryPlaceSDK.addInfoSection({ + id: 'custom-welcome', + title: '👋 Welcome!', + position: 'top', + content: ` +

+ Welcome to our custom FurryPlace server! This is a demonstration of how plugins + can add custom content to the info modal. +

+ ` + }); + } + + // Example 2: Section with interactive content + function addStatsSection() { + window.FurryPlaceSDK.addInfoSection({ + id: 'server-stats', + title: '📊 Server Statistics', + position: 'after-video', + content: () => { + const div = document.createElement('div'); + div.className = 'text-sm space-y-2'; + + const stats = [ + { label: 'Total Players', value: '10,000+' }, + { label: 'Pixels Painted', value: '5,000,000+' }, + { label: 'Active Alliances', value: '250' } + ]; + + stats.forEach(stat => { + const statDiv = document.createElement('div'); + statDiv.className = 'flex justify-between items-center p-2 bg-base-200 rounded'; + statDiv.innerHTML = ` + ${stat.label}: + ${stat.value} + `; + div.appendChild(statDiv); + }); + + return div; + } + }); + } + + // Example 3: Custom links section + function addLinksSection() { + window.FurryPlaceSDK.addInfoSection({ + id: 'custom-links', + title: '🔗 Useful Links', + position: 'bottom', + content: ` +
+ + 📚 Wiki + + + 💬 Discord + + + 🐦 Twitter + +
+ ` + }); + } + + // Example 4: Dynamic content with user state + function addUserInfoSection() { + window.FurryPlaceSDK.addInfoSection({ + id: 'user-quick-info', + title: '👤 Your Info', + position: 'after-video', + content: () => { + const div = document.createElement('div'); + div.className = 'text-sm'; + + // Check if user is logged in + if (!window.FurryPlaceSDK.isLoggedIn()) { + div.innerHTML = '

Please log in to see your stats

'; + return div; + } + + const droplets = window.FurryPlaceSDK.getDroplets(); + const level = window.FurryPlaceSDK.getLevel(); + const charges = window.FurryPlaceSDK.getCharges(); + + div.innerHTML = ` +
+
+
Level
+
${level}
+
+
+
Droplets
+
${droplets}
+
+
+
Paint Charges
+
${charges.current}/${charges.max}
+
+
+
+
+
+ `; + + return div; + } + }); + } + + // Example 5: Announcement section + function addAnnouncementSection() { + window.FurryPlaceSDK.addInfoSection({ + id: 'announcements', + title: '📢 Announcements', + position: 'top', + className: 'bg-primary/10 p-4 rounded-lg', + content: ` +
+
+ + + + New Event: Double droplets weekend starts Friday! +
+
+ + + + Server maintenance completed successfully +
+
+ ` + }); + } + + // Example 6: Tips & Tricks section + function addTipsSection() { + window.FurryPlaceSDK.addInfoSection({ + id: 'tips-tricks', + title: '💡 Tips & Tricks', + position: 'bottom', + content: ` + + ` + }); + } + + // Example 7: Changelog section + function addChangelogSection() { + window.FurryPlaceSDK.addInfoSection({ + id: 'changelog', + title: '📝 Recent Updates', + position: 'bottom', + content: ` +
+
+
+ v2.0.0 - Latest +
+
    +
  • Added plugin system
  • +
  • Improved performance
  • +
  • Bug fixes
  • +
+
+
+
+ v1.9.0 +
+
    +
  • Alliance system improvements
  • +
  • New color palette
  • +
+
+
+
+
+ ` + }); + } + + // Example 8: Embed section with custom styling + function addFeaturedArtSection() { + window.FurryPlaceSDK.addInfoSection({ + id: 'featured-art', + title: '🎨 Featured Artwork', + position: 'after-video', + content: ` +
+
+

Check out this amazing artwork created by our community!

+
+
+ 🖼️ +
+
+ 🎭 +
+
+ 🌈 +
+
+
+
+ ` + }); + } + + // Initialize plugin + waitForSDK(() => { + if (EXAMPLE_DISABLED) { + console.log('[Info Modal Plugin] Disabled - set EXAMPLE_DISABLED to false to enable'); + return; + } + + console.log('[Info Modal Plugin] Initializing...'); + + // Register all example sections + // Comment out the ones you don't want to use + addWelcomeSection(); + addAnnouncementSection(); + addStatsSection(); + addUserInfoSection(); + addTipsSection(); + addLinksSection(); + addFeaturedArtSection(); + addChangelogSection(); + + console.log('[Info Modal Plugin] Loaded successfully!'); + console.log('[Info Modal Plugin] Registered sections:', window.FurryPlaceSDK.getInfoSections()); + }); +})(); diff --git a/frontend-backup/plugins/example-user-state.js b/frontend-backup/plugins/example-user-state.js new file mode 100644 index 0000000..cb9a1c3 --- /dev/null +++ b/frontend-backup/plugins/example-user-state.js @@ -0,0 +1,224 @@ +/** + * Example FurryPlace Plugin - User State Demo + * + * This demonstrates how to use the FurryPlace SDK to access user state + * including droplets, charges, level, and more. + * + * To use this plugin, add it to your HTML: + * + */ + +(function() { + 'use strict'; + + // Set this to true to disable this example plugin (for reference only) + const EXAMPLE_DISABLED = true; + + // Wait for SDK to be available + function waitForSDK(callback) { + if (window.FurryPlaceSDK) { + callback(); + } else { + setTimeout(() => waitForSDK(callback), 100); + } + } + + // Example 1: Button that shows user info (only visible when logged in) + function registerUserInfoButton() { + window.FurryPlaceSDK.registerButton({ + id: 'user-info-display', + title: 'Show My Stats', + position: 'after-leaderboard', + icon: ` + + + + `, + onClick: async () => { + const user = await window.FurryPlaceSDK.getUser(); + const charges = window.FurryPlaceSDK.getCharges(); + + const info = ` +🎨 Player Stats +━━━━━━━━━━━━━━━━━━ +👤 Name: ${user.name} +🏆 Level: ${user.level} +🎯 Pixels Painted: ${user.pixelsPainted} +💧 Droplets: ${user.droplets} + +⚡ Paint Charges +━━━━━━━━━━━━━━━━━━ +Current: ${charges.current}/${charges.max} +Percentage: ${charges.percentage.toFixed(1)}% +Can Paint: ${window.FurryPlaceSDK.hasChargesToPaint() ? 'Yes ✅' : 'No ❌'} + +🏰 Alliance +━━━━━━━━━━━━━━━━━━ +In Alliance: ${window.FurryPlaceSDK.isInAlliance() ? 'Yes' : 'No'} +${user.allianceId ? `Alliance ID: ${user.allianceId}\nRole: ${user.allianceRole}` : ''} + `.trim(); + + alert(info); + console.log('Full user data:', user); + }, + condition: (context) => context.user?.isLoggedIn + }); + } + + // Example 2: Droplet counter button + function registerDropletCounter() { + window.FurryPlaceSDK.registerButton({ + id: 'droplet-counter', + title: 'View Droplets', + position: 'before-leaderboard', + icon: ` + + + + `, + onClick: () => { + const droplets = window.FurryPlaceSDK.getDroplets(); + alert(`💧 You have ${droplets} droplets!`); + }, + condition: (context) => context.user?.isLoggedIn + }); + } + + // Example 3: Charge status indicator + function registerChargeStatus() { + window.FurryPlaceSDK.registerButton({ + id: 'charge-status', + title: 'Paint Charge Status', + position: 'bottom', + icon: ` + + + + `, + onClick: () => { + const charges = window.FurryPlaceSDK.getCharges(); + const canPaint = window.FurryPlaceSDK.hasChargesToPaint(); + + const statusMsg = ` +⚡ Paint Charges Status +━━━━━━━━━━━━━━━━━━ +Current: ${charges.current}/${charges.max} +Filled: ${charges.percentage.toFixed(1)}% +Cooldown: ${charges.cooldownMs}ms +Status: ${canPaint ? '✅ Ready to paint!' : '❌ Recharging...'} + `.trim(); + + alert(statusMsg); + }, + condition: (context) => context.user?.isLoggedIn + }); + } + + // Example 4: Level display button + function registerLevelDisplay() { + window.FurryPlaceSDK.registerButton({ + id: 'level-display', + title: 'View Level Progress', + position: 'top', + icon: ` + + + + `, + onClick: () => { + const level = window.FurryPlaceSDK.getLevel(); + const pixelsPainted = window.FurryPlaceSDK.getPixelsPainted(); + + // Calculate next level (based on formula: level = floor(sqrt(pixels/100)) + 1) + const nextLevel = level + 1; + const pixelsForNextLevel = Math.pow(nextLevel - 1, 2) * 100; + const pixelsNeeded = pixelsForNextLevel - pixelsPainted; + + const progress = ` +🏆 Level Progress +━━━━━━━━━━━━━━━━━━ +Current Level: ${level} +Pixels Painted: ${pixelsPainted} +Next Level: ${nextLevel} +Pixels Needed: ${pixelsNeeded > 0 ? pixelsNeeded : 0} + `.trim(); + + alert(progress); + }, + condition: (context) => context.user?.isLoggedIn + }); + } + + // Example 5: Login status toggle + function registerLoginStatus() { + window.FurryPlaceSDK.registerButton({ + id: 'login-status', + title: 'Login Required', + position: 'bottom', + className: 'btn btn-square btn-warning shadow-md', + icon: ` + + + + `, + onClick: () => { + alert('Please log in to access FurryPlace features!'); + }, + condition: (context) => !context.user?.isLoggedIn + }); + } + + // Example 6: Refresh user data button + function registerRefreshButton() { + window.FurryPlaceSDK.registerButton({ + id: 'refresh-user-data', + title: 'Refresh User Data', + position: 'after-leaderboard', + icon: ` + + + + `, + onClick: async () => { + const user = await window.FurryPlaceSDK.refreshUser(); + if (user) { + alert(`✅ User data refreshed!\n\nDroplets: ${user.droplets}\nCharges: ${user.charges.count}/${user.charges.max}`); + } + }, + condition: (context) => context.user?.isLoggedIn + }); + } + + // Initialize plugin + waitForSDK(() => { + if (EXAMPLE_DISABLED) { + console.log('[User State Plugin] Disabled - set EXAMPLE_DISABLED to false to enable'); + return; + } + + console.log('[User State Plugin] Initializing...'); + + // Register all example buttons + registerUserInfoButton(); + registerDropletCounter(); + registerChargeStatus(); + registerLevelDisplay(); + registerLoginStatus(); + registerRefreshButton(); + + console.log('[User State Plugin] Loaded successfully!'); + + // Log initial user state + setTimeout(async () => { + const isLoggedIn = window.FurryPlaceSDK.isLoggedIn(); + console.log('[User State Plugin] User logged in:', isLoggedIn); + + if (isLoggedIn) { + console.log('[User State Plugin] Droplets:', window.FurryPlaceSDK.getDroplets()); + console.log('[User State Plugin] Charges:', window.FurryPlaceSDK.getCharges()); + console.log('[User State Plugin] Level:', window.FurryPlaceSDK.getLevel()); + console.log('[User State Plugin] Can paint:', window.FurryPlaceSDK.hasChargesToPaint()); + } + }, 1000); + }); +})(); diff --git a/frontend-backup/plugins/login-captcha.js b/frontend-backup/plugins/login-captcha.js new file mode 100644 index 0000000..c513132 --- /dev/null +++ b/frontend-backup/plugins/login-captcha.js @@ -0,0 +1,476 @@ +/** + * FurryPlace Plugin - Login Modal Captcha + * + * This plugin adds a captcha widget to the login modal and disables + * all login buttons until the captcha is completed. + */ + +(function() { + 'use strict'; + + // Get configuration + const config = window.FURRYPLACE_CAPTCHA_CONFIG?.login || { enabled: false }; + + if (!config.enabled || !config.siteKey) { + console.log('[Login Captcha] Captcha not enabled for login modal'); + return; + } + + let hcaptchaLoaded = false; + let currentToken = null; + let widgetId = null; + + // Load hCaptcha script + function loadHCaptchaScript() { + return new Promise((resolve, reject) => { + if (window.hcaptcha) { + hcaptchaLoaded = true; + resolve(); + return; + } + + const script = document.createElement('script'); + script.src = 'https://js.hcaptcha.com/1/api.js?render=explicit'; + script.async = true; + script.defer = true; + script.onload = () => { + console.log('[Login Captcha] hCaptcha script loaded'); + hcaptchaLoaded = true; + resolve(); + }; + script.onerror = () => { + console.error('[Login Captcha] Failed to load hCaptcha script'); + reject(new Error('Failed to load hCaptcha script')); + }; + document.head.appendChild(script); + }); + } + + // Wait for hCaptcha API to be ready + function waitForHCaptcha() { + return new Promise((resolve) => { + const check = () => { + if (window.hcaptcha && window.hcaptcha.render) { + resolve(); + } else { + setTimeout(check, 100); + } + }; + check(); + }); + } + + // Find login buttons in modal + function findLoginButtons(modal) { + const buttons = []; + + // Find all links with /auth/ in href (OAuth buttons) + const authLinks = modal.querySelectorAll('a[href*="/auth/"]'); + + for (const el of authLinks) { + const href = el.href || ''; + + // Only include actual OAuth provider links + if ( + href.includes('/auth/google') || + href.includes('/auth/twitch') || + href.includes('/auth/discord') || + href.includes('/auth/github') + ) { + buttons.push(el); + } + } + + // Also check for explicit login/register buttons (not just links) + const loginButtons = modal.querySelectorAll('button[type="submit"], button.btn-primary'); + for (const btn of loginButtons) { + const text = btn.textContent.toLowerCase(); + if (text.includes('login') || text.includes('sign in') || text.includes('register')) { + buttons.push(btn); + } + } + + return buttons; + } + + // Disable all login buttons + function disableLoginButtons(buttons) { + for (const btn of buttons) { + btn.disabled = true; + btn.style.opacity = '0.5'; + btn.style.cursor = 'not-allowed'; + btn.dataset.captchaDisabled = 'true'; + + // Store original click handler and prevent clicks + btn.addEventListener('click', preventClick, { capture: true }); + } + } + + // Enable all login buttons and update URLs with captcha token + function enableLoginButtons(buttons, token = null) { + for (const btn of buttons) { + btn.disabled = false; + btn.style.opacity = '1'; + btn.style.cursor = 'pointer'; + delete btn.dataset.captchaDisabled; + + // Update URL with captcha token if provided + if (token && btn.href) { + const url = new URL(btn.href, window.location.origin); + url.searchParams.set('token', token); + btn.href = url.toString(); + } + + // Remove click prevention + btn.removeEventListener('click', preventClick, { capture: true }); + } + } + + // Prevent click event + function preventClick(e) { + if (e.target.dataset.captchaDisabled === 'true') { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + alert('Please complete the captcha verification first'); + return false; + } + } + + // Create captcha widget container + function createCaptchaContainer() { + const container = document.createElement('div'); + container.className = 'login-captcha-container'; + container.style.cssText = ` + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + width: 100%; + `; + + const label = document.createElement('p'); + label.textContent = 'Please verify you are human to continue:'; + label.style.cssText = ` + margin: 0 0 0.5rem 0; + font-size: 0.875rem; + color: #000000; + text-align: center; + `; + container.appendChild(label); + + const widgetContainer = document.createElement('div'); + widgetContainer.className = 'hcaptcha-widget'; + container.appendChild(widgetContainer); + + return { container, widgetContainer }; + } + + // Inject captcha into login modal + async function injectCaptchaIntoModal(modal) { + // Check if already injected + if (modal.querySelector('.login-captcha-container')) { + console.log('[Login Captcha] Captcha already injected in this modal'); + return; + } + + console.log('[Login Captcha] Injecting captcha into login modal'); + + // Find login buttons + const buttons = findLoginButtons(modal); + if (buttons.length === 0) { + console.log('[Login Captcha] No login buttons found in modal'); + return; + } + + console.log('[Login Captcha] Found', buttons.length, 'login button(s)'); + + // Disable buttons initially + disableLoginButtons(buttons); + + // Create captcha container + const { container, widgetContainer } = createCaptchaContainer(); + + // Find a good place to insert the captcha + // Look for the container that holds the buttons + let buttonsContainer = null; + + // Try to find the parent container of the buttons (usually has flex classes) + for (const btn of buttons) { + let parent = btn.parentElement; + while (parent && parent !== modal) { + if (parent.classList.contains('flex') || parent.classList.contains('gap-2')) { + buttonsContainer = parent; + break; + } + parent = parent.parentElement; + } + if (buttonsContainer) break; + } + + // Insert captcha inside the buttons container after the last button + if (buttonsContainer) { + // Find the last OAuth button + const lastButton = buttons[buttons.length - 1]; + + // Remove any empty divs that might be placeholders + const emptyDivs = buttonsContainer.querySelectorAll('div.mt-2.flex.flex-col.items-center.gap-1'); + emptyDivs.forEach(div => { + if (div.children.length === 0 || (div.children.length <= 2 && div.textContent.trim() === '')) { + div.remove(); + } + }); + + // Insert after the last button + if (lastButton.nextSibling) { + buttonsContainer.insertBefore(container, lastButton.nextSibling); + } else { + buttonsContainer.appendChild(container); + } + } else { + // Fallback: try to insert inside the form after buttons + const form = modal.querySelector('form'); + if (form) { + // Find last button and insert after it + const lastButton = buttons[buttons.length - 1]; + if (lastButton.parentElement) { + if (lastButton.nextSibling) { + lastButton.parentElement.insertBefore(container, lastButton.nextSibling); + } else { + lastButton.parentElement.appendChild(container); + } + } else { + form.appendChild(container); + } + } else { + // Last resort: append to modal body + const body = modal.querySelector('.modal-box') || modal; + body.appendChild(container); + } + } + + // Wait for hCaptcha to be ready + await waitForHCaptcha(); + + // Render hCaptcha widget + try { + widgetId = window.hcaptcha.render(widgetContainer, { + sitekey: config.siteKey, + theme: config.theme || 'dark', + size: config.size || 'normal', + callback: function(token) { + currentToken = token; + console.log('[Login Captcha] Captcha verified, token:', token.substring(0, 20) + '...'); + + // Update button URLs with the token + enableLoginButtons(buttons, token); + + // Store token for backend requests + sessionStorage.setItem('login_captcha_token', token); + + // Trigger custom event + window.dispatchEvent(new CustomEvent('furryplace:captcha:verified', { + detail: { token, verificationPoint: 'login' } + })); + }, + 'error-callback': function() { + console.error('[Login Captcha] Captcha error'); + currentToken = null; + disableLoginButtons(buttons); + sessionStorage.removeItem('login_captcha_token'); + }, + 'expired-callback': function() { + console.warn('[Login Captcha] Captcha expired'); + currentToken = null; + disableLoginButtons(buttons); + sessionStorage.removeItem('login_captcha_token'); + } + }); + + console.log('[Login Captcha] hCaptcha widget rendered'); + } catch (error) { + console.error('[Login Captcha] Failed to render hCaptcha:', error); + // On error, enable buttons so user isn't stuck + enableLoginButtons(buttons); + } + } + + // Watch for login modals appearing + function watchForLoginModals() { + // Create a mutation observer to watch for modals + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + // Check added nodes + for (const node of mutation.addedNodes) { + if (node.nodeType === 1) { // Element node + // Check if it's a dialog/modal + if (node.tagName === 'DIALOG' || node.classList?.contains('modal')) { + checkAndInjectCaptcha(node); + } + + // Check children for dialogs + const dialogs = node.querySelectorAll?.('dialog, .modal') || []; + for (const dialog of dialogs) { + checkAndInjectCaptcha(dialog); + } + } + } + + // Check for 'open' attribute changes on dialogs + if (mutation.type === 'attributes' && mutation.attributeName === 'open') { + const target = mutation.target; + if (target.tagName === 'DIALOG' && target.open) { + checkAndInjectCaptcha(target); + } + } + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['open'] + }); + + console.log('[Login Captcha] Watching for login modals'); + } + + // Check if modal is a login modal and inject captcha + async function checkAndInjectCaptcha(modal) { + // Wait a bit for modal content to be fully rendered + await new Promise(resolve => setTimeout(resolve, 100)); + + console.log('[Login Captcha] Checking modal for login buttons...', modal); + + // Check if modal contains login-related content + const buttons = findLoginButtons(modal); + console.log('[Login Captcha] Found buttons:', buttons); + + if (buttons.length > 0) { + injectCaptchaIntoModal(modal); + } else { + console.log('[Login Captcha] No login buttons found in this modal'); + } + } + + // Initialize plugin + async function init() { + console.log('[Login Captcha] Initializing login modal captcha plugin...'); + + try { + // Load hCaptcha script + await loadHCaptchaScript(); + + // Start watching for modals + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', watchForLoginModals); + } else { + watchForLoginModals(); + } + + // Also check existing modals + const existingModals = document.querySelectorAll('dialog, .modal'); + for (const modal of existingModals) { + if (modal.open || modal.classList.contains('modal-open')) { + checkAndInjectCaptcha(modal); + } + } + + // Register SDK captcha callback for programmatic requests + if (window.FurryPlaceSDK) { + window.FurryPlaceSDK.registerCaptchaCallback('login', async () => { + // Return stored token if available + const storedToken = sessionStorage.getItem('login_captcha_token'); + if (storedToken) { + return storedToken; + } + + // Otherwise return current token from widget + return currentToken; + }); + + // Also register for Google OAuth + window.FurryPlaceSDK.registerCaptchaCallback('googleOAuth', async () => { + const storedToken = sessionStorage.getItem('login_captcha_token'); + if (storedToken) { + return storedToken; + } + return currentToken; + }); + + // Also register for register + window.FurryPlaceSDK.registerCaptchaCallback('register', async () => { + const storedToken = sessionStorage.getItem('login_captcha_token'); + if (storedToken) { + return storedToken; + } + return currentToken; + }); + + console.log('[Login Captcha] Registered SDK captcha callbacks'); + } + + // Intercept fetch requests to inject captcha token + const originalFetch = window.fetch; + window.fetch = async function(url, options) { + const urlString = typeof url === 'string' ? url : url.toString(); + + // Check if this is a login or auth request + if (urlString.includes('/login') || urlString.includes('/auth/')) { + const token = sessionStorage.getItem('login_captcha_token'); + if (token && options) { + // Add token to request body if it's a POST + if (options.method === 'POST' && options.body) { + try { + const bodyData = typeof options.body === 'string' ? JSON.parse(options.body) : options.body; + bodyData.captchaToken = token; + options.body = JSON.stringify(bodyData); + } catch (e) { + console.warn('[Login Captcha] Could not inject captcha token into request body'); + } + } + } + } + + return originalFetch.call(this, url, options); + }; + + // Intercept links to /auth/google to add token as query param + document.addEventListener('click', function(e) { + let target = e.target; + while (target && target !== document) { + if (target.tagName === 'A' && target.href && target.href.includes('/auth/google')) { + const token = sessionStorage.getItem('login_captcha_token'); + if (token) { + e.preventDefault(); + const url = new URL(target.href); + url.searchParams.set('token', token); + window.location.href = url.toString(); + } + return; + } + target = target.parentElement; + } + }, true); + + console.log('[Login Captcha] Plugin initialized'); + } catch (error) { + console.error('[Login Captcha] Failed to initialize:', error); + } + } + + // Wait for SDK if needed, then init + if (window.FurryPlaceSDK) { + init(); + } else { + const waitForSDK = () => { + if (window.FurryPlaceSDK) { + init(); + } else { + setTimeout(waitForSDK, 100); + } + }; + waitForSDK(); + } +})(); diff --git a/frontend-backup/plugins/pixel-lock.js b/frontend-backup/plugins/pixel-lock.js new file mode 100644 index 0000000..7604228 --- /dev/null +++ b/frontend-backup/plugins/pixel-lock.js @@ -0,0 +1,671 @@ +/** + * Pixel Lock Plugin + * + * This plugin adds a pixel locking button to the FurryPlace interface. + * Users can click the button to enter lock mode, select pixels to lock, + * and protect them from being painted over for 24 hours. + * + * Cost: 5 droplets per pixel + * Duration: 24 hours + * Cooldown: 24 hours after unlock + */ + +(function() { + 'use strict'; + + // State + let lockMode = false; + let currentMode = 'rect'; // 'rect', 'brush', 'pixel' + let selectedPixels = new Set(); + let currentTile = { x: 1024, y: 1024 }; + let lockedPixelsCache = new Map(); + let brushSize = 1; + + // UI Elements + let lockModeUI = null; + let lockOverlay = null; + let lockCanvas = null; + + // Icons + const lockIcon = ` + + + + `; + + const unlockIcon = ` + + + + `; + + // Create lock mode GUI + function createLockModeUI() { + const ui = document.createElement('div'); + ui.id = 'pixel-lock-ui'; + ui.innerHTML = ` + + +

+ + + + Lock Mode Active +

+ +
+ 🟢 Select pixels to lock (5 💧 per pixel) +
+ +
+ +
+ + + +
+
+ + + +
+
+ Pixels selected: + 0 +
+
+ Cost per pixel: + 5 💧 +
+
+ Total cost: + 0 💧 +
+
+ Your droplets: + 0 💧 +
+
+ +
+ + +
+ +
+ +
+ +
+ How to use:
+ • Rect: Drag to select rectangular area
+ • Brush: Click and drag to paint selection
+ • Pixel: Click individual pixels +
+ `; + + document.body.appendChild(ui); + lockModeUI = ui; + + // Set up event listeners + setupUIEventListeners(); + updateCostPreview(); + } + + function setupUIEventListeners() { + // Mode selection + const modeButtons = lockModeUI.querySelectorAll('.mode-btn'); + modeButtons.forEach(btn => { + btn.addEventListener('click', () => { + modeButtons.forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + currentMode = btn.dataset.mode; + + // Show/hide brush size control + const brushSizeContainer = document.getElementById('brush-size-container'); + brushSizeContainer.style.display = currentMode === 'brush' ? 'block' : 'none'; + + updateCursor(); + }); + }); + + // Brush size + const brushSlider = document.getElementById('brush-size-slider'); + const brushDisplay = document.getElementById('brush-size-display'); + brushSlider.addEventListener('input', (e) => { + brushSize = parseInt(e.target.value); + brushDisplay.textContent = brushSize; + }); + + // Lock button + document.getElementById('lock-confirm-btn').addEventListener('click', confirmLock); + + // Clear button + document.getElementById('lock-clear-btn').addEventListener('click', () => { + selectedPixels.clear(); + updateCostPreview(); + renderOverlay(); + }); + + // Exit button + document.getElementById('lock-exit-btn').addEventListener('click', () => { + disableLockMode(); + }); + } + + function updateCursor() { + if (!lockCanvas) return; + + const cursors = { + rect: 'crosshair', + brush: 'cell', + pixel: 'pointer' + }; + + lockCanvas.style.cursor = cursors[currentMode] || 'crosshair'; + } + + function updateCostPreview() { + const pixelCount = selectedPixels.size; + const totalCost = pixelCount * 5; + const userDroplets = window.FurryPlaceSDK.getDroplets(); + + document.getElementById('pixels-selected').textContent = pixelCount; + document.getElementById('total-cost').textContent = `${totalCost} 💧`; + document.getElementById('user-droplets').textContent = `${userDroplets} 💧`; + + const lockBtn = document.getElementById('lock-confirm-btn'); + lockBtn.disabled = pixelCount === 0 || totalCost > userDroplets; + } + + async function confirmLock() { + if (selectedPixels.size === 0) return; + + const coords = Array.from(selectedPixels).flatMap(key => { + const [x, y] = key.split(',').map(Number); + return [x, y]; + }); + + const cost = selectedPixels.size * 5; + const droplets = window.FurryPlaceSDK.getDroplets(); + + if (cost > droplets) { + alert(`Not enough droplets! Need ${cost} 💧, you have ${droplets} 💧.`); + return; + } + + try { + const lockBtn = document.getElementById('lock-confirm-btn'); + lockBtn.disabled = true; + lockBtn.textContent = '🔒 Locking...'; + + const result = await window.FurryPlaceSDK.lockPixels( + currentTile.x, + currentTile.y, + coords + ); + + alert(`✅ Successfully locked ${result.locked} pixel(s) for ${result.cost} 💧!`); + + selectedPixels.clear(); + await refreshLockedPixels(); + updateCostPreview(); + renderOverlay(); + + lockBtn.textContent = '🔒 Lock Pixels'; + } catch (error) { + alert(`❌ Failed to lock pixels: ${error.message}`); + document.getElementById('lock-confirm-btn').disabled = false; + document.getElementById('lock-confirm-btn').textContent = '🔒 Lock Pixels'; + } + } + + function createLockOverlay() { + const overlay = document.createElement('div'); + overlay.id = 'pixel-lock-overlay'; + overlay.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 10000; + `; + + const canvas = document.createElement('canvas'); + canvas.id = 'pixel-lock-canvas'; + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + canvas.style.cssText = ` + position: absolute; + top: 0; + left: 0; + pointer-events: auto; + cursor: crosshair; + `; + + overlay.appendChild(canvas); + document.body.appendChild(overlay); + lockOverlay = overlay; + lockCanvas = canvas; + + setupCanvasEventListeners(); + return overlay; + } + + function setupCanvasEventListeners() { + let isDragging = false; + let dragStart = null; + let lastBrushPos = null; + + lockCanvas.addEventListener('mousedown', (e) => { + isDragging = true; + dragStart = { x: e.clientX, y: e.clientY }; + lastBrushPos = { x: e.clientX, y: e.clientY }; + + if (currentMode === 'pixel') { + // Single pixel selection + const pixelKey = `${Math.floor(e.clientX)},${Math.floor(e.clientY)}`; + if (selectedPixels.has(pixelKey)) { + selectedPixels.delete(pixelKey); + } else { + selectedPixels.add(pixelKey); + } + updateCostPreview(); + renderOverlay(); + } else if (currentMode === 'brush') { + // Start brush stroke + addBrushPixels(e.clientX, e.clientY); + updateCostPreview(); + renderOverlay(); + } + }); + + lockCanvas.addEventListener('mousemove', (e) => { + if (!isDragging) return; + + if (currentMode === 'rect') { + renderOverlay({ x1: dragStart.x, y1: dragStart.y, x2: e.clientX, y2: e.clientY }); + } else if (currentMode === 'brush') { + addBrushPixels(e.clientX, e.clientY); + lastBrushPos = { x: e.clientX, y: e.clientY }; + updateCostPreview(); + renderOverlay(); + } + }); + + lockCanvas.addEventListener('mouseup', (e) => { + if (!isDragging) return; + isDragging = false; + + if (currentMode === 'rect') { + const x1 = Math.min(dragStart.x, e.clientX); + const y1 = Math.min(dragStart.y, e.clientY); + const x2 = Math.max(dragStart.x, e.clientX); + const y2 = Math.max(dragStart.y, e.clientY); + + for (let x = Math.floor(x1); x <= Math.floor(x2); x++) { + for (let y = Math.floor(y1); y <= Math.floor(y2); y++) { + const key = `${x},${y}`; + selectedPixels.add(key); + } + } + + updateCostPreview(); + renderOverlay(); + } + }); + + lockCanvas.addEventListener('mouseleave', () => { + isDragging = false; + renderOverlay(); + }); + } + + function addBrushPixels(x, y) { + const centerX = Math.floor(x); + const centerY = Math.floor(y); + const radius = Math.floor(brushSize / 2); + + for (let dx = -radius; dx <= radius; dx++) { + for (let dy = -radius; dy <= radius; dy++) { + const distance = Math.sqrt(dx * dx + dy * dy); + if (distance <= brushSize / 2) { + const key = `${centerX + dx},${centerY + dy}`; + selectedPixels.add(key); + } + } + } + } + + function renderOverlay(dragRect = null) { + if (!lockCanvas) return; + + const ctx = lockCanvas.getContext('2d'); + ctx.clearRect(0, 0, lockCanvas.width, lockCanvas.height); + + // Draw selected pixels (green) + ctx.fillStyle = 'rgba(74, 222, 128, 0.5)'; + for (const key of selectedPixels) { + const [x, y] = key.split(',').map(Number); + ctx.fillRect(x, y, 1, 1); + } + + // Draw drag rect preview + if (dragRect && currentMode === 'rect') { + const x = Math.min(dragRect.x1, dragRect.x2); + const y = Math.min(dragRect.y1, dragRect.y2); + const width = Math.abs(dragRect.x2 - dragRect.x1); + const height = Math.abs(dragRect.y2 - dragRect.y1); + + ctx.fillStyle = 'rgba(74, 222, 128, 0.3)'; + ctx.fillRect(x, y, width, height); + + ctx.strokeStyle = 'rgba(74, 222, 128, 0.8)'; + ctx.lineWidth = 2; + ctx.strokeRect(x, y, width, height); + } + } + + async function refreshLockedPixels() { + try { + const result = await window.FurryPlaceSDK.getLockedPixels( + currentTile.x, + currentTile.y + ); + lockedPixelsCache.set(`${currentTile.x},${currentTile.y}`, result.locked); + } catch (error) { + console.error('Failed to refresh locked pixels:', error); + } + } + + function removeLockOverlay() { + if (lockOverlay) { + lockOverlay.remove(); + lockOverlay = null; + lockCanvas = null; + } + } + + function removeLockModeUI() { + if (lockModeUI) { + lockModeUI.remove(); + lockModeUI = null; + } + } + + async function enableLockMode() { + lockMode = true; + await refreshLockedPixels(); + createLockOverlay(); + createLockModeUI(); + console.log('[Pixel Lock] Lock mode enabled'); + } + + function disableLockMode() { + lockMode = false; + removeLockOverlay(); + removeLockModeUI(); + selectedPixels.clear(); + console.log('[Pixel Lock] Lock mode disabled'); + + // Update button + const button = document.querySelector('[data-furryplace-button="pixel-lock"] button'); + if (button) { + button.classList.remove('btn-active'); + button.innerHTML = lockIcon; + } + } + + async function toggleLockMode() { + if (lockMode) { + disableLockMode(); + } else { + enableLockMode(); + + // Update button + const button = document.querySelector('[data-furryplace-button="pixel-lock"] button'); + if (button) { + button.classList.add('btn-active'); + button.innerHTML = unlockIcon; + } + } + } + + // Initialize plugin + function init() { + if (!window.FurryPlaceSDK) { + console.error('[Pixel Lock] FurryPlace SDK not found'); + return; + } + + console.log('[Pixel Lock] Initializing pixel lock plugin...'); + + window.FurryPlaceSDK.registerButton({ + id: 'pixel-lock', + title: 'Lock Pixels (5 droplets per pixel, 24h duration)', + icon: lockIcon, + position: 'before-leaderboard', + onClick: async (context) => { + if (!context.user.isLoggedIn) { + alert('Please log in to use pixel locking'); + return; + } + + await toggleLockMode(); + }, + condition: (context) => context.user?.isLoggedIn, + className: 'btn btn-square shadow-md' + }); + + console.log('[Pixel Lock] Plugin initialized successfully'); + } + + // Initialize when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + + // Refresh locked pixels periodically + setInterval(async () => { + if (lockMode) { + await refreshLockedPixels(); + } + }, 30000); + +})();