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: `
+
+ `
+ });
+ }
+
+ // 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 = `
+
+
+
+
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: `
+
+ Join an alliance to collaborate with other players
+ Paint in your flag's region for 10% charge discount
+ Level up to increase your max paint charges
+ Use keyboard shortcuts for faster painting
+
+ `
+ });
+ }
+
+ // 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)
+
+
+
+
Selection Mode:
+
+
+ 📐 Rect
+
+
+ 🖌️ Brush
+
+
+ ⬜ Pixel
+
+
+
+
+
+
Brush Size:
+
+
Size: 1 pixels
+
+
+
+
+ Pixels selected:
+ 0
+
+
+ Cost per pixel:
+ 5 💧
+
+
+ Total cost:
+ 0 💧
+
+
+ Your droplets:
+ 0 💧
+
+
+
+
+
+ 🔒 Lock Pixels
+
+
+ Clear
+
+
+
+
+
+ Exit Lock Mode
+
+
+
+
+ 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);
+
+})();