Files
my_openplace/frontend-backup/plugins/README.md
T
2025-10-05 00:58:08 -07:00

15 KiB

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

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

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

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

Cloudflare Turnstile captcha integration:

  • Free and privacy-friendly
  • Lightweight and fast
  • Automatic theme detection
  • Configure per verification point

Configuration:

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

hCaptcha integration:

  • Privacy-focused alternative
  • Customizable themes and sizes
  • Supports compact mode

Configuration:

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

Google reCAPTCHA v2 and v3 integration:

  • Support for both v2 (checkbox) and v3 (invisible)
  • Per-action configuration for v3
  • Customizable themes for v2

Configuration:

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:

{
  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):

{
  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:

{
  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:

{
  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:

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

(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: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-5">
        <path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z"/>
      </svg>`,
      onClick: (context) => {
        alert('Button clicked!');
      }
    });
  });
})();

Accessing User State

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

// 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

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

window.FurryPlaceSDK.addInfoSection({
  id: 'welcome-message',
  title: 'Welcome!',
  position: 'top',
  content: '<p class="text-sm">Welcome to our server!</p>'
});

Dynamic Content with Function

window.FurryPlaceSDK.addInfoSection({
  id: 'user-stats',
  title: 'Your Stats',
  position: 'after-video',
  content: () => {
    const level = window.FurryPlaceSDK.getLevel();
    const droplets = window.FurryPlaceSDK.getDroplets();

    return `
      <div class="text-sm">
        <p>Level: ${level}</p>
        <p>Droplets: ${droplets}</p>
      </div>
    `;
  }
});

Interactive Section with DOM Elements

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:

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 <script> tags
  3. Reports loading progress in the browser console

You can check which plugins are loaded by looking at the console:

[FurryPlace SDK] Auto-discovering plugins...
[FurryPlace SDK] Found 2 plugin(s): ['example-button.js', 'my-plugin.js']
[FurryPlace SDK] Loaded plugin: example-button.js
[FurryPlace SDK] Loaded plugin: my-plugin.js

Note: If you need to disable a plugin, either delete it or rename it to something other than .js (like .js.disabled)

Troubleshooting

Button not appearing?

  • Check the browser console for errors
  • Verify the SDK is loaded: console.log(window.FurryPlaceSDK)
  • Ensure your button ID is unique
  • Try different positions

Button appearing in wrong place?

  • Try different position values
  • Check if the button container has changed in the UI

Icon not showing?

  • Verify your SVG has viewBox and fill="currentColor"
  • Add class="size-5" to the SVG element
  • Check for syntax errors in the SVG string