This commit is contained in:
2025-10-06 16:17:29 -07:00
parent 61acdd7111
commit 31f25ad472
5 changed files with 1637 additions and 7 deletions
-7
View File
@@ -1,7 +0,0 @@
# Ignore all plugin files except the example
*
!.gitignore
!README.md
!example-button.js
|login-captcha.js
|pixel-lock.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:
* <script src="/plugins/example-info-modal.js"></script>
*/
(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: `
<p class="text-sm">
Welcome to our custom FurryPlace server! This is a demonstration of how plugins
can add custom content to the info modal.
</p>
`
});
}
// 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 = `
<span class="font-medium">${stat.label}:</span>
<span class="text-primary font-bold">${stat.value}</span>
`;
div.appendChild(statDiv);
});
return div;
}
});
}
// Example 3: Custom links section
function addLinksSection() {
window.FurryPlaceSDK.addInfoSection({
id: 'custom-links',
title: '🔗 Useful Links',
position: 'bottom',
content: `
<div class="flex flex-wrap gap-2">
<a href="https://github.com/yourserver/wiki" target="_blank" class="btn btn-sm btn-outline">
📚 Wiki
</a>
<a href="https://discord.gg/yourserver" target="_blank" class="btn btn-sm btn-outline">
💬 Discord
</a>
<a href="https://twitter.com/yourserver" target="_blank" class="btn btn-sm btn-outline">
🐦 Twitter
</a>
</div>
`
});
}
// 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 = '<p class="text-warning">Please log in to see your stats</p>';
return div;
}
const droplets = window.FurryPlaceSDK.getDroplets();
const level = window.FurryPlaceSDK.getLevel();
const charges = window.FurryPlaceSDK.getCharges();
div.innerHTML = `
<div class="grid grid-cols-2 gap-2">
<div class="bg-base-200 p-2 rounded">
<div class="text-xs opacity-70">Level</div>
<div class="text-lg font-bold text-primary">${level}</div>
</div>
<div class="bg-base-200 p-2 rounded">
<div class="text-xs opacity-70">Droplets</div>
<div class="text-lg font-bold text-primary">${droplets}</div>
</div>
<div class="bg-base-200 p-2 rounded col-span-2">
<div class="text-xs opacity-70">Paint Charges</div>
<div class="text-lg font-bold text-primary">${charges.current}/${charges.max}</div>
<div class="w-full bg-base-300 rounded-full h-1.5 mt-1">
<div class="bg-primary h-1.5 rounded-full" style="width: ${charges.percentage}%"></div>
</div>
</div>
</div>
`;
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: `
<div class="text-sm space-y-2">
<div class="alert alert-info py-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span><strong>New Event:</strong> Double droplets weekend starts Friday!</span>
</div>
<div class="alert alert-success py-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>Server maintenance completed successfully</span>
</div>
</div>
`
});
}
// Example 6: Tips & Tricks section
function addTipsSection() {
window.FurryPlaceSDK.addInfoSection({
id: 'tips-tricks',
title: '💡 Tips & Tricks',
position: 'bottom',
content: `
<ul class="text-sm list-disc list-inside space-y-1">
<li>Join an alliance to collaborate with other players</li>
<li>Paint in your flag's region for 10% charge discount</li>
<li>Level up to increase your max paint charges</li>
<li>Use keyboard shortcuts for faster painting</li>
</ul>
`
});
}
// Example 7: Changelog section
function addChangelogSection() {
window.FurryPlaceSDK.addInfoSection({
id: 'changelog',
title: '📝 Recent Updates',
position: 'bottom',
content: `
<div class="text-sm">
<div class="space-y-2">
<details class="collapse collapse-arrow bg-base-200">
<summary class="collapse-title text-sm font-medium">v2.0.0 - Latest</summary>
<div class="collapse-content text-xs">
<ul class="list-disc list-inside mt-2">
<li>Added plugin system</li>
<li>Improved performance</li>
<li>Bug fixes</li>
</ul>
</div>
</details>
<details class="collapse collapse-arrow bg-base-200">
<summary class="collapse-title text-sm font-medium">v1.9.0</summary>
<div class="collapse-content text-xs">
<ul class="list-disc list-inside mt-2">
<li>Alliance system improvements</li>
<li>New color palette</li>
</ul>
</div>
</details>
</div>
</div>
`
});
}
// Example 8: Embed section with custom styling
function addFeaturedArtSection() {
window.FurryPlaceSDK.addInfoSection({
id: 'featured-art',
title: '🎨 Featured Artwork',
position: 'after-video',
content: `
<div class="text-sm">
<div class="bg-base-200 rounded-lg p-3">
<p class="mb-2">Check out this amazing artwork created by our community!</p>
<div class="flex gap-2 overflow-x-auto">
<div class="bg-base-300 w-20 h-20 rounded flex-shrink-0 flex items-center justify-center">
🖼️
</div>
<div class="bg-base-300 w-20 h-20 rounded flex-shrink-0 flex items-center justify-center">
🎭
</div>
<div class="bg-base-300 w-20 h-20 rounded flex-shrink-0 flex items-center justify-center">
🌈
</div>
</div>
</div>
</div>
`
});
}
// 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());
});
})();
@@ -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:
* <script src="/plugins/example-user-state.js"></script>
*/
(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: `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-5">
<path d="M480-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM160-160v-112q0-34 17.5-62.5T224-378q62-31 126-46.5T480-440q66 0 130 15.5T736-378q29 15 46.5 43.5T800-272v112H160Zm80-80h480v-32q0-11-5.5-20T700-306q-54-27-109-40.5T480-360q-56 0-111 13.5T260-306q-9 5-14.5 14t-5.5 20v32Zm240-320q33 0 56.5-23.5T560-640q0-33-23.5-56.5T480-720q-33 0-56.5 23.5T400-640q0 33 23.5 56.5T480-560Zm0-80Zm0 400Z"/>
</svg>
`,
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: `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-5">
<path d="M480-80q-137 0-228.5-94T160-408q0-100 79.5-217.5T480-880q161 137 240.5 254.5T800-408q0 140-91.5 234T480-80Zm0-80q104 0 172-70.5T720-408q0-73-60.5-165T480-774Q361-665 300.5-573T240-408q0 107 68 177.5T480-160Zm0-320Z"/>
</svg>
`,
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: `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-5">
<path d="M280-360q-100 0-170-70T40-600q0-100 70-170t170-70h400q100 0 170 70t70 170q0 100-70 170t-170 70H280Zm0-80h400q66 0 113-47t47-113q0-66-47-113t-113-47H280q-66 0-113 47t-47 113q0 66 47 113t113 47Zm400-40q42 0 71-29t29-71q0-42-29-71t-71-29q-42 0-71 29t-29 71q0 42 29 71t71 29ZM480-600Z"/>
</svg>
`,
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: `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-5">
<path d="m354-287 126-76 126 77-33-144 111-96-146-13-58-136-58 135-146 13 111 97-33 143ZM233-120l65-281L80-590l288-25 112-265 112 265 288 25-218 189 65 281-247-149-247 149Zm247-350Z"/>
</svg>
`,
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: `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-5">
<path d="M480-120v-80h280v-560H480v-80h280q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H480Zm-80-160-55-58 102-102H120v-80h327L345-622l55-58 200 200-200 200Z"/>
</svg>
`,
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: `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-5">
<path d="M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z"/>
</svg>
`,
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);
});
})();
+476
View File
@@ -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();
}
})();
+671
View File
@@ -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 = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
<path fill-rule="evenodd" d="M12 1.5a5.25 5.25 0 0 0-5.25 5.25v3a3 3 0 0 0-3 3v6.75a3 3 0 0 0 3 3h10.5a3 3 0 0 0 3-3v-6.75a3 3 0 0 0-3-3v-3c0-2.9-2.35-5.25-5.25-5.25Zm3.75 8.25v-3a3.75 3.75 0 1 0-7.5 0v3h7.5Z" clip-rule="evenodd" />
</svg>
`;
const unlockIcon = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
<path d="M18 1.5c2.9 0 5.25 2.35 5.25 5.25v3.75a.75.75 0 0 1-1.5 0V6.75a3.75 3.75 0 1 0-7.5 0v3a3 3 0 0 1 3 3v6.75a3 3 0 0 1-3 3H3.75a3 3 0 0 1-3-3v-6.75a3 3 0 0 1 3-3h9v-3c0-2.9 2.35-5.25 5.25-5.25Z" />
</svg>
`;
// Create lock mode GUI
function createLockModeUI() {
const ui = document.createElement('div');
ui.id = 'pixel-lock-ui';
ui.innerHTML = `
<style>
#pixel-lock-ui {
position: fixed;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.9);
border: 2px solid #4ade80;
border-radius: 12px;
padding: 16px;
color: white;
font-family: system-ui, -apple-system, sans-serif;
z-index: 10001;
min-width: 280px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}
#pixel-lock-ui h3 {
margin: 0 0 12px 0;
font-size: 18px;
font-weight: 600;
color: #4ade80;
display: flex;
align-items: center;
gap: 8px;
}
#pixel-lock-ui .status {
font-size: 14px;
margin-bottom: 16px;
padding: 8px;
background: rgba(74, 222, 128, 0.2);
border-radius: 6px;
border-left: 3px solid #4ade80;
}
#pixel-lock-ui .mode-selector {
margin-bottom: 16px;
}
#pixel-lock-ui .mode-selector label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
}
#pixel-lock-ui .mode-buttons {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
#pixel-lock-ui .mode-btn {
padding: 8px 12px;
background: rgba(255, 255, 255, 0.1);
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 6px;
color: white;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
text-align: center;
}
#pixel-lock-ui .mode-btn:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.5);
}
#pixel-lock-ui .mode-btn.active {
background: #4ade80;
border-color: #4ade80;
color: black;
font-weight: 600;
}
#pixel-lock-ui .brush-size {
margin-bottom: 16px;
}
#pixel-lock-ui .brush-size label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
}
#pixel-lock-ui .brush-size input {
width: 100%;
accent-color: #4ade80;
}
#pixel-lock-ui .brush-size-value {
text-align: center;
margin-top: 4px;
font-size: 12px;
color: #4ade80;
}
#pixel-lock-ui .cost-preview {
margin-bottom: 16px;
padding: 12px;
background: rgba(96, 165, 250, 0.2);
border-radius: 6px;
border: 1px solid rgba(96, 165, 250, 0.5);
}
#pixel-lock-ui .cost-preview .row {
display: flex;
justify-content: space-between;
margin-bottom: 6px;
font-size: 14px;
}
#pixel-lock-ui .cost-preview .row:last-child {
margin-bottom: 0;
padding-top: 6px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
font-weight: 600;
color: #4ade80;
}
#pixel-lock-ui .actions {
display: flex;
gap: 8px;
}
#pixel-lock-ui .btn {
flex: 1;
padding: 10px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
#pixel-lock-ui .btn-lock {
background: #4ade80;
color: black;
}
#pixel-lock-ui .btn-lock:hover:not(:disabled) {
background: #22c55e;
}
#pixel-lock-ui .btn-lock:disabled {
opacity: 0.5;
cursor: not-allowed;
}
#pixel-lock-ui .btn-clear {
background: rgba(239, 68, 68, 0.8);
color: white;
}
#pixel-lock-ui .btn-clear:hover {
background: rgba(239, 68, 68, 1);
}
#pixel-lock-ui .btn-exit {
background: rgba(255, 255, 255, 0.1);
color: white;
}
#pixel-lock-ui .btn-exit:hover {
background: rgba(255, 255, 255, 0.2);
}
#pixel-lock-ui .instructions {
margin-top: 12px;
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
line-height: 1.5;
}
</style>
<h3>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" style="width: 20px; height: 20px;">
<path fill-rule="evenodd" d="M12 1.5a5.25 5.25 0 0 0-5.25 5.25v3a3 3 0 0 0-3 3v6.75a3 3 0 0 0 3 3h10.5a3 3 0 0 0 3-3v-6.75a3 3 0 0 0-3-3v-3c0-2.9-2.35-5.25-5.25-5.25Zm3.75 8.25v-3a3.75 3.75 0 1 0-7.5 0v3h7.5Z" clip-rule="evenodd" />
</svg>
Lock Mode Active
</h3>
<div class="status">
🟢 Select pixels to lock (5 💧 per pixel)
</div>
<div class="mode-selector">
<label>Selection Mode:</label>
<div class="mode-buttons">
<button class="mode-btn active" data-mode="rect">
📐<br>Rect
</button>
<button class="mode-btn" data-mode="brush">
🖌️<br>Brush
</button>
<button class="mode-btn" data-mode="pixel">
⬜<br>Pixel
</button>
</div>
</div>
<div class="brush-size" id="brush-size-container" style="display: none;">
<label>Brush Size:</label>
<input type="range" id="brush-size-slider" min="1" max="10" value="1">
<div class="brush-size-value">Size: <span id="brush-size-display">1</span> pixels</div>
</div>
<div class="cost-preview">
<div class="row">
<span>Pixels selected:</span>
<span id="pixels-selected">0</span>
</div>
<div class="row">
<span>Cost per pixel:</span>
<span>5 💧</span>
</div>
<div class="row">
<span>Total cost:</span>
<span id="total-cost">0 💧</span>
</div>
<div class="row">
<span>Your droplets:</span>
<span id="user-droplets">0 💧</span>
</div>
</div>
<div class="actions">
<button class="btn btn-lock" id="lock-confirm-btn" disabled>
🔒 Lock Pixels
</button>
<button class="btn btn-clear" id="lock-clear-btn">
Clear
</button>
</div>
<div class="actions" style="margin-top: 8px;">
<button class="btn btn-exit" id="lock-exit-btn">
Exit Lock Mode
</button>
</div>
<div class="instructions">
<strong>How to use:</strong><br>
• <strong>Rect:</strong> Drag to select rectangular area<br>
• <strong>Brush:</strong> Click and drag to paint selection<br>
• <strong>Pixel:</strong> Click individual pixels
</div>
`;
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);
})();