wewe
This commit is contained in:
@@ -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);
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user