Files
my_openplace/frontend-backup/plugins/pixel-lock.js
T
2025-10-06 16:17:29 -07:00

672 lines
18 KiB
JavaScript

/**
* 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);
})();