# FurryPlace Plugins This directory contains plugins that extend the FurryPlace UI using the FurryPlace SDK. ## Quick Start 1. **Create a plugin file** in this directory (e.g., `my-plugin.js`) 2. **Refresh the page** - Plugins are automatically discovered and loaded! 3. **Use the SDK** to register buttons or add other functionality > **✨ Auto-Loading:** Plugins are automatically discovered from the `frontend-backup/plugins/` directory. Any `.js` file you add here will be automatically loaded when the page loads. No need to manually edit HTML files! ## Example Plugins ### [example-button.js](./example-button.js) Complete example showing how to: - Add custom buttons with icons - Handle click events - Use conditional rendering - Apply custom styling - Open external links ### [example-user-state.js](./example-user-state.js) Demonstrates user state access: - Display user stats and information - Show droplet counter - Monitor paint charge status - Display level progress - Conditional rendering based on login status - Refresh user data from server ### [example-info-modal.js](./example-info-modal.js) Demonstrates info modal customization: - Add custom sections to the info modal - Display announcements and updates - Show server statistics - Add custom links and resources - Display user-specific information - Create interactive collapsible sections - Add tips, tricks, and changelogs ## Captcha Plugins FurryPlace includes built-in support for captcha verification at different points: - **Login**: When logging into an existing account - **Registration**: When creating a new account - **Paint**: When painting pixels (can be configured to require every N paints) - **Google OAuth**: Before initiating Google OAuth flow ### [captcha-turnstile.js](./captcha-turnstile.js) Cloudflare Turnstile captcha integration: - Free and privacy-friendly - Lightweight and fast - Automatic theme detection - Configure per verification point **Configuration:** ```javascript window.FURRYPLACE_CAPTCHA_CONFIG = { login: { enabled: true, siteKey: 'your-turnstile-site-key', theme: 'auto', // 'light', 'dark', or 'auto' }, register: { enabled: true, siteKey: 'your-turnstile-site-key', theme: 'auto', }, paint: { enabled: true, siteKey: 'your-turnstile-site-key', theme: 'dark', }, googleOAuth: { enabled: true, siteKey: 'your-turnstile-site-key', theme: 'light', } }; ``` ### [captcha-hcaptcha.js](./captcha-hcaptcha.js) hCaptcha integration: - Privacy-focused alternative - Customizable themes and sizes - Supports compact mode **Configuration:** ```javascript window.FURRYPLACE_CAPTCHA_CONFIG = { login: { enabled: true, siteKey: 'your-hcaptcha-site-key', theme: 'light', // 'light' or 'dark' size: 'normal', // 'normal', 'compact', or 'invisible' }, register: { enabled: true, siteKey: 'your-hcaptcha-site-key', theme: 'dark', size: 'normal', }, paint: { enabled: true, siteKey: 'your-hcaptcha-site-key', theme: 'dark', size: 'compact', } }; ``` ### [captcha-recaptcha.js](./captcha-recaptcha.js) Google reCAPTCHA v2 and v3 integration: - Support for both v2 (checkbox) and v3 (invisible) - Per-action configuration for v3 - Customizable themes for v2 **Configuration:** ```javascript window.FURRYPLACE_CAPTCHA_CONFIG = { login: { enabled: true, siteKey: 'your-recaptcha-site-key', version: 'v2', // 'v2' or 'v3' theme: 'light', // v2 only size: 'normal', // v2 only action: 'login', // v3 only }, paint: { enabled: true, siteKey: 'your-recaptcha-v3-site-key', version: 'v3', action: 'paint', } }; ``` **Important:** Only load ONE captcha plugin at a time. Choose the one that matches your backend configuration. ## FurryPlace SDK API ### Button Registration #### `window.FurryPlaceSDK.registerButton(config)` Register a custom button in the UI. **Configuration Object:** ```javascript { id: string, // Required: Unique identifier title: string, // Required: Tooltip text icon: string|HTMLElement, // Required: SVG string or DOM element onClick: Function, // Required: (context, event) => {} position: string, // Optional: 'top', 'bottom', 'before-leaderboard', 'after-leaderboard' className: string, // Optional: Custom CSS classes (default: 'btn btn-square shadow-md') wrapperClass: string, // Optional: Wrapper div CSS classes condition: Function, // Optional: (context) => boolean disabled: boolean // Optional: Whether button is disabled } ``` **Positions:** - `'top'` - First button in the container - `'bottom'` - Last button in the container - `'before-leaderboard'` - Before the leaderboard button - `'after-leaderboard'` - After the leaderboard button (default) **Context Object** (passed to `onClick` and `condition`): ```javascript { sdk: { version: string, // SDK version refreshUserState: Function // Function to refresh user state }, user: { isLoggedIn: boolean, // User login status id: number|null, // User ID name: string|null, // Display name discord: string|null, // Discord username country: string|null, // User's country banned: boolean, // Is user banned role: string|null, // User role (admin/moderator/user) level: number, // Current level pixelsPainted: number, // Total pixels painted droplets: number, // Currency amount picture: string|null, // Profile picture URL equippedFlag: string|null, // Equipped country flag allianceId: number|null, // Alliance ID allianceRole: string|null, // Alliance role charges: { current: number, // Current paint charges max: number, // Maximum charges cooldownMs: number, // Cooldown in milliseconds percentage: number // Charge percentage (0-100) }, hasChargesToPaint: boolean, // Can paint (>= 1 charge) needsPhoneVerification: boolean, // Needs phone verification isTimedOut: boolean, // Currently timed out timeoutUntil: string|null // Timeout expiry ISO string }, map: {} // Map context (future use) } ``` ### User State Methods #### `await FurryPlaceSDK.getUser()` Get full user data (async). Returns user object or `null` if not logged in. #### `await FurryPlaceSDK.refreshUser()` Refresh user state from server and return updated data. #### `FurryPlaceSDK.isLoggedIn()` Returns `true` if user is logged in. #### `FurryPlaceSDK.getDroplets()` Get user's droplet count (currency). #### `FurryPlaceSDK.getCharges()` Get paint charge information: ```javascript { current: number, // Current charges max: number, // Max capacity cooldownMs: number, // Recharge cooldown percentage: number // Fill percentage (0-100) } ``` #### `FurryPlaceSDK.hasChargesToPaint()` Returns `true` if user has at least 1 charge. #### `FurryPlaceSDK.getLevel()` Get user's current level. #### `FurryPlaceSDK.getPixelsPainted()` Get total pixels painted by user. #### `FurryPlaceSDK.getAllianceId()` Get user's alliance ID or `null`. #### `FurryPlaceSDK.isInAlliance()` Returns `true` if user is in an alliance. ### Info Modal Customization #### `FurryPlaceSDK.addInfoSection(config)` Add a custom section to the info modal (the one with rules and YouTube video). **Configuration Object:** ```javascript { id: string, // Required: Unique identifier title: string, // Optional: Section title content: string|HTMLElement|Function, // Required: HTML string, DOM element, or function position: string, // Optional: 'top', 'bottom', 'after-video' className: string // Optional: Custom CSS classes for section } ``` **Positions:** - `'top'` - At the beginning of the modal - `'bottom'` - At the end (before footer) - `'after-video'` - Right after the YouTube video section **Content Types:** - **String**: HTML string that will be inserted - **HTMLElement**: DOM element to append - **Function**: Function that returns a string or HTMLElement #### `FurryPlaceSDK.removeInfoSection(id)` Remove a custom section from the info modal. #### `FurryPlaceSDK.getInfoSections()` Get list of all registered info sections. ### Captcha Methods #### `FurryPlaceSDK.registerCaptchaCallback(verificationPoint, callback)` Register a callback function that provides captcha tokens for a specific verification point. **Parameters:** - `verificationPoint`: One of `'login'`, `'register'`, `'googleOAuth'`, or `'paint'` - `callback`: Async function that returns a captcha token string **Example:** ```javascript window.FurryPlaceSDK.registerCaptchaCallback('login', async () => { // Show captcha UI and wait for user to complete it const token = await showCaptchaWidget(); return token; }); ``` #### `await FurryPlaceSDK.requestCaptcha(verificationPoint)` Request a captcha token for a specific verification point. This calls the registered callback. **Returns:** Promise that resolves to a captcha token string or `null` if no callback is registered. #### `FurryPlaceSDK.hasCaptchaCallback(verificationPoint)` Check if a captcha callback is registered for a verification point. **Returns:** `true` if callback is registered, `false` otherwise. #### `FurryPlaceSDK.unregisterCaptchaCallback(verificationPoint)` Remove a captcha callback for a verification point. #### `FurryPlaceSDK.getCaptchaCallbacks()` Get list of all verification points with registered callbacks. **Returns:** Array of strings (verification point names). ### Other SDK Methods - `FurryPlaceSDK.unregisterButton(id)` - Remove a registered button - `FurryPlaceSDK.getButtons()` - Get list of all registered buttons - `FurryPlaceSDK.getContext()` - Get current context object - `FurryPlaceSDK.init()` - Manually initialize SDK (usually automatic) ## Example Usage ### Basic Button ```javascript (function() { 'use strict'; function waitForSDK(callback) { if (window.FurryPlaceSDK) { callback(); } else { setTimeout(() => waitForSDK(callback), 100); } } waitForSDK(() => { window.FurryPlaceSDK.registerButton({ id: 'my-custom-button', title: 'My Button', position: 'bottom', icon: ` `, onClick: (context) => { alert('Button clicked!'); } }); }); })(); ``` ### Accessing User State ```javascript waitForSDK(() => { // Synchronous methods console.log('Logged in:', window.FurryPlaceSDK.isLoggedIn()); console.log('Droplets:', window.FurryPlaceSDK.getDroplets()); console.log('Level:', window.FurryPlaceSDK.getLevel()); // Async method for full user data window.FurryPlaceSDK.getUser().then(user => { if (user) { console.log('User name:', user.name); console.log('Alliance:', user.allianceId); } }); }); ``` ### Button with User State Condition ```javascript // Only show button if user is logged in and has charges window.FurryPlaceSDK.registerButton({ id: 'paint-helper', title: 'Quick Paint', position: 'top', icon: `...`, onClick: async (context) => { const charges = context.user.charges; alert(`You have ${charges.current}/${charges.max} charges`); }, condition: (context) => { return context.user?.isLoggedIn && context.user?.hasChargesToPaint; } }); ``` ### Charge Monitor Example ```javascript window.FurryPlaceSDK.registerButton({ id: 'charge-monitor', title: 'Charge Monitor', position: 'bottom', icon: `...`, onClick: () => { const charges = window.FurryPlaceSDK.getCharges(); alert(` Current: ${charges.current}/${charges.max} Percentage: ${charges.percentage.toFixed(1)}% Can Paint: ${window.FurryPlaceSDK.hasChargesToPaint() ? 'Yes' : 'No'} `); }, condition: (context) => context.user?.isLoggedIn }); ``` ### Info Modal Section Examples #### Simple HTML Section ```javascript window.FurryPlaceSDK.addInfoSection({ id: 'welcome-message', title: 'Welcome!', position: 'top', content: '

Welcome to our server!

' }); ``` #### Dynamic Content with Function ```javascript window.FurryPlaceSDK.addInfoSection({ id: 'user-stats', title: 'Your Stats', position: 'after-video', content: () => { const level = window.FurryPlaceSDK.getLevel(); const droplets = window.FurryPlaceSDK.getDroplets(); return `

Level: ${level}

Droplets: ${droplets}

`; } }); ``` #### Interactive Section with DOM Elements ```javascript window.FurryPlaceSDK.addInfoSection({ id: 'interactive-section', title: 'Custom Links', position: 'bottom', content: () => { const div = document.createElement('div'); div.className = 'flex gap-2'; const button = document.createElement('button'); button.className = 'btn btn-sm btn-primary'; button.textContent = 'Click Me'; button.onclick = () => alert('Button clicked!'); div.appendChild(button); return div; } }); ``` ## Finding SVG Icons You can find Material Design icons at: - https://fonts.google.com/icons Copy the SVG code and use it as the `icon` parameter. ## Tips 1. **Wrap your plugin in an IIFE** to avoid polluting the global scope 2. **Wait for the SDK** to be available before registering buttons 3. **Use unique IDs** to avoid conflicts with other plugins 4. **Test button positions** to find the best placement for your use case 5. **Check the console** for SDK loading messages and errors ## How Auto-Loading Works The SDK automatically: 1. Calls `/api/plugins` to get a list of all `.js` files in the `plugins/` directory 2. Dynamically loads each plugin by creating `