Overlay c/ Paleta Manual V13 (Minimizável)

Adiciona a funcionalidade de minimizar e maximizar a barra lateral de cores.

// ==UserScript==
// @name         Overlay c/ Paleta Manual V13 (Minimizável)
// @namespace    http://tampermonkey.net/
// @version      13.0
// @description  Adiciona a funcionalidade de minimizar e maximizar a barra lateral de cores.
// @author       Víkish (com modificações de Gemini)
// @match        https://wplace.live/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=partidomissao.com
// @license      MIT
// @grant        none
// ==/UserScript==

(async function () {
    'use strict';

    // #####################################################################
    // ##### EDITE SUA PALETA DE CORES FIXA AQUI #####
    // #####################################################################
    const PALETA_DE_CORES_FIXA = [
        { nome: "Mostrar Todos", valor: 'all', rgb: 'all' },
        { nome: "Preto", valor: [0, 0, 0], rgb: "rgb(0, 0, 0)" },
        { nome: "Cinza Escuro", valor: [60, 60, 60], rgb: "rgb(60, 60, 60)" },
        { nome: "Marrom Escuro", valor: [104, 70, 52], rgb: "rgb(104, 70, 52)" },
        { nome: "Marrom", valor: [149, 104, 42], rgb: "rgb(149, 104, 42)" },
        { nome: "Cinza", valor: [120, 120, 120], rgb: "rgb(120, 120, 120)" },
        { nome: "Pele", valor: [248, 178, 119], rgb: "rgb(248, 178, 119)" },
        { nome: "Amarelo Queimado", valor: [246, 170, 9], rgb: "rgb(246, 170, 9)" },
        { nome: "Rosa Claro", valor: [243, 141, 169], rgb: "rgb(243, 141, 169)" },
        { nome: "Creme", valor: [255, 250, 188], rgb: "rgb(255, 250, 188)" },
        { nome: "Vinho", valor: [96, 0, 24], rgb: "rgb(96, 0, 24)" },
        { nome: "Cinza Claro", valor: [210, 210, 210], rgb: "rgb(210, 210, 210)" },
        { nome: "Amarelo", valor: [249, 221, 59], rgb: "rgb(249, 221, 59)" },
        { nome: "Branco", valor: [255, 255, 255], rgb: "rgb(255, 255, 255)" },
        { nome: "Azul Escuro", valor: [40, 80, 158], rgb: "rgb(40, 80, 158)" },
        { nome: "Laranja", valor: [255, 127, 39], rgb: "rgb(255, 127, 39)" },
        { nome: "Laranja Escuro", valor: [227, 129, 17], rgb: "rgb(227, 129, 17)" },
        { nome: "Vermelho", valor: [237, 28, 36], rgb: "rgb(237, 28, 36)" },
        { nome: "Magenta", valor: [236, 31, 128], rgb: "rgb(236, 31, 128)" },
        { nome: "Roxo", valor: [107, 80, 246], rgb: "rgb(107, 80, 246)" },
        { nome: "Lilás", valor: [153, 177, 251], rgb: "rgb(153, 177, 251)" },
        { nome: "Azul", valor: [64, 147, 228], rgb: "rgb(64, 147, 228)" },
        { nome: "Verde Limão", valor: [135, 255, 94], rgb: "rgb(135, 255, 94)" },
    ];
    // #####################################################################


    const CORRECT_PIXEL_COLOR = [0, 255, 0, 255];
    const OVERLAY_MODES = ["overlay", "original", "chunks"];
    let overlayMode = OVERLAY_MODES[0];
    let isSidebarMinimized = false;

    const selectedColors = new Set();
    const missions = await fetchData();
    const colorCounters = {};
    const workingPalette = PALETA_DE_CORES_FIXA;

    workingPalette.forEach(p => {
        if (p.valor !== 'all') {
            colorCounters[p.rgb] = { wrongPixelsInChunk: 0, totalPixelsInChunk: 0 };
        }
        selectedColors.add(p.rgb);
    });

    console.log("Pré-processando gabarito (quantização para paleta fixa)...");
    for (const mission of missions) {
        mission.quantizedImageData = await quantizeImage(mission.imageData, workingPalette);
    }
    console.log("Pré-processamento concluído.");

    // --- UI (Interface do Usuário) ---
    createPaletteSidebar(workingPalette);
    const counterContainer = document.createElement("div");
    Object.assign(counterContainer.style, { position: "fixed", top: "5px", left: "50%", transform: "translateX(-50%)", zIndex: "1000", padding: "6px 10px", fontSize: "12px", fontFamily: "Arial, sans-serif", backgroundColor: "#000a", color: "white", borderRadius: "6px", pointerEvents: "none", backdropFilter: "blur(3px)", lineHeight: "1.4" });
    document.body.appendChild(counterContainer);
    const pixelCounter = document.createElement("div");
    counterContainer.appendChild(pixelCounter);
    const percentageCounter = document.createElement("div");
    counterContainer.appendChild(percentageCounter);
    patchModeButtonUI();


    // --- Lógica Principal (Proxy para interceptar o 'fetch') ---
    fetch = new Proxy(fetch, {
        apply: async (target, thisArg, argList) => {
            const urlString = typeof argList[0] === "object" ? argList[0].url : argList[0];
            let url;
            try { url = new URL(urlString); } catch (e) { return target.apply(thisArg, argList); }

            if (overlayMode === 'overlay' && url.hostname === "backend.wplace.live" && url.pathname.startsWith("/files/")) {
                const relevantMissions = missions.filter(m => url.pathname.endsWith(`/${m.chunk[0]}/${m.chunk[1]}.png`));
                if (relevantMissions.length > 0) {
                    const originalResponse = await target.apply(thisArg, argList);
                    const originalBlob = await originalResponse.blob();
                    const originalImage = await blobToImage(originalBlob);
                    const { width, height } = originalImage;
                    const canvas = new OffscreenCanvas(width, height);
                    const ctx = canvas.getContext("2d", { willReadFrequently: true });
                    ctx.drawImage(originalImage, 0, 0, width, height);
                    const originalData = ctx.getImageData(0, 0, width, height);
                    const resultData = ctx.getImageData(0, 0, width, height);
                    const combinedTemplate = await getCombinedTemplateForChunk(relevantMissions, width, height);
                    processPixels(originalData, combinedTemplate, resultData);
                    updateCountersUI();
                    ctx.putImageData(resultData, 0, 0);
                    const mergedBlob = await canvas.convertToBlob();
                    return new Response(mergedBlob, { headers: { "Content-Type": "image/png" } });
                }
            } else if (overlayMode === 'chunks' && url.hostname === "backend.wplace.live" && url.pathname.startsWith("/files/")) {
                const CHUNK_WIDTH = 1000, CHUNK_HEIGHT = 1000;
                const parts = url.pathname.split("/");
                const [chunk1, chunk2] = [parts.at(-2), parts.at(-1).split(".")[0]];
                const canvas = new OffscreenCanvas(CHUNK_WIDTH, CHUNK_HEIGHT);
                const ctx = canvas.getContext("2d");
                ctx.strokeStyle = 'red'; ctx.lineWidth = 2;
                ctx.strokeRect(0, 0, CHUNK_WIDTH, CHUNK_HEIGHT);
                ctx.font = '40px Arial'; ctx.fillStyle = 'red';
                ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
                ctx.fillText(`${chunk1}, ${chunk2}`, CHUNK_WIDTH / 2, CHUNK_HEIGHT / 2);
                const mergedBlob = await canvas.convertToBlob();
                return new Response(mergedBlob, { headers: { "Content-Type": "image/png" } });
            }
            return target.apply(thisArg, argList);
        }
    });


    // --- FUNÇÕES ---

    function colorDistance(rgb1, rgb2) {
        return Math.sqrt(Math.pow(rgb1[0] - rgb2[0], 2) + Math.pow(rgb1[1] - rgb2[1], 2) + Math.pow(rgb1[2] - rgb2[2], 2));
    }

    function findClosestPaletteColor(rgb, palette) {
        let minDistance = Infinity;
        let closestColor = null;
        const paletteColors = palette.filter(p => p.valor !== 'all');
        for (const pColor of paletteColors) {
            const dist = colorDistance(rgb, pColor.valor);
            if (dist < minDistance) {
                minDistance = dist;
                closestColor = pColor.valor;
            }
        }
        return closestColor;
    }

    async function quantizeImage(imageData, palette) {
        const quantizedData = new ImageData(imageData.width, imageData.height);
        const data = imageData.data;
        const qData = quantizedData.data;
        for (let i = 0; i < data.length; i += 4) {
            if (data[i + 3] > 0) {
                const pixelRgb = [data[i], data[i + 1], data[i + 2]];
                const closestColor = findClosestPaletteColor(pixelRgb, palette);
                if (closestColor) {
                    qData[i] = closestColor[0];
                    qData[i + 1] = closestColor[1];
                    qData[i + 2] = closestColor[2];
                    qData[i + 3] = 255;
                }
            }
        }
        return quantizedData;
    }

    async function getCombinedTemplateForChunk(relevantMissions, width, height) {
        const combinedTemplate = new OffscreenCanvas(width, height).getContext('2d').getImageData(0, 0, width, height);
        for (const mission of relevantMissions) {
            const templateCanvas = new OffscreenCanvas(1000, 1000);
            templateCanvas.getContext('2d').putImageData(mission.quantizedImageData, 0, 0);
            const missionImg = await blobToImage(await templateCanvas.convertToBlob());
            const finalCanvas = new OffscreenCanvas(width, height);
            const finalCtx = finalCanvas.getContext('2d');
            finalCtx.drawImage(missionImg, mission.coords[0], mission.coords[1]);
            const missionData = finalCtx.getImageData(0, 0, width, height).data;
            for (let i = 0; i < missionData.length; i += 4) {
                if (missionData[i + 3] > 0) {
                    combinedTemplate.data[i] = missionData[i];
                    combinedTemplate.data[i + 1] = missionData[i + 1];
                    combinedTemplate.data[i + 2] = missionData[i + 2];
                    combinedTemplate.data[i + 3] = missionData[i + 3];
                }
            }
        }
        return combinedTemplate;
    }


    function processPixels(original, template, result) {
        const d1 = original.data, d2 = template.data, dr = result.data;
        for (const key in colorCounters) {
            colorCounters[key].wrongPixelsInChunk = 0;
            colorCounters[key].totalPixelsInChunk = 0;
        }
        for (let i = 0; i < d1.length; i += 4) {
            if (d2[i + 3] > 0) {
                const templateColorRgb = `rgb(${d2[i]}, ${d2[i+1]}, ${d2[i+2]})`;
                const isSelected = selectedColors.has('all') || selectedColors.has(templateColorRgb);
                const isCorrect = d1[i] === d2[i] && d1[i + 1] === d2[i + 1] && d1[i + 2] === d2[i + 2];
                if (colorCounters[templateColorRgb]) {
                    colorCounters[templateColorRgb].totalPixelsInChunk++;
                    if (!isCorrect) {
                        colorCounters[templateColorRgb].wrongPixelsInChunk++;
                    }
                }
                if (isSelected) {
                    if (isCorrect) {
                        [dr[i], dr[i + 1], dr[i + 2], dr[i+3]] = CORRECT_PIXEL_COLOR;
                    } else {
                        dr[i] = d2[i]; dr[i + 1] = d2[i + 1]; dr[i + 2] = d2[i + 2]; dr[i + 3] = 255;
                    }
                } else {
                    [dr[i], dr[i + 1], dr[i + 2], dr[i+3]] = CORRECT_PIXEL_COLOR;
                }
            }
        }
    }


    async function fetchData() {
        const response = await fetch("https://gist.githubusercontent.com/yl99a/45ec3df57cc75c4b93c45251b87eb20b/raw/overlays.json?" + Date.now());
        const missionsData = await response.json();
        for(const mission of missionsData) {
            const { img, width, height } = await loadImage(mission.url);
            const canvas = new OffscreenCanvas(width, height);
            const ctx = canvas.getContext("2d");
            ctx.drawImage(img, 0, 0, width, height);
            mission.imageData = ctx.getImageData(0, 0, width, height);
        }
        return missionsData;
    }


    function createPaletteSidebar(palette) {
        const sidebar = document.createElement("div");
        sidebar.id = "overlay-sidebar";
        Object.assign(sidebar.style, {
            position: "fixed", top: "70px", left: "10px", zIndex: "1000",
            backgroundColor: "#000000bb", color: "white", borderRadius: "8px",
            padding: "10px", fontFamily: "Arial, sans-serif", fontSize: "14px",
            backdropFilter: "blur(5px)", transition: "min-width 0.2s"
        });

        // --- CABEÇALHO DA SIDEBAR ---
        const header = document.createElement('div');
        Object.assign(header.style, {
            display: 'flex', justifyContent: 'space-between', alignItems: 'center',
            paddingBottom: '8px', marginBottom: '8px', borderBottom: '1px solid #ffffff33'
        });

        const title = document.createElement('h3');
        title.textContent = "Paleta de Cores";
        Object.assign(title.style, { margin: 0, fontSize: '16px' });

        const toggleButton = document.createElement('button');
        toggleButton.textContent = '–'; // Símbolo de minimizar
        Object.assign(toggleButton.style, {
            background: 'none', border: '1px solid #fff5', color: 'white',
            cursor: 'pointer', fontSize: '18px', width: '24px', height: '24px',
            borderRadius: '4px', lineHeight: '20px', padding: '0'
        });

        header.appendChild(title);
        header.appendChild(toggleButton);
        sidebar.appendChild(header);

        // --- CONTEÚDO DA SIDEBAR (LISTA DE CORES) ---
        const content = document.createElement('div');
        content.id = "sidebar-content";
        Object.assign(content.style, {
            maxHeight: "calc(100vh - 250px)", overflowY: "auto"
        });

        toggleButton.addEventListener('click', () => {
            isSidebarMinimized = !isSidebarMinimized;
            if (isSidebarMinimized) {
                content.style.display = 'none';
                toggleButton.textContent = '+';
                sidebar.style.minWidth = 'auto';
            } else {
                content.style.display = 'block';
                toggleButton.textContent = '–';
            }
        });


        const colorCheckboxes = [];
        const masterCheckboxContainer = document.createElement("input");

        palette.forEach((pColor, index) => {
            const entryDiv = document.createElement("div");
            Object.assign(entryDiv.style, { marginBottom: "8px", display: "flex", alignItems: "center" });
            const checkbox = document.createElement("input");
            checkbox.type = "checkbox"; checkbox.checked = true; checkbox.id = `color-checkbox-${index}`;
            Object.assign(checkbox.style, { marginRight: "8px", cursor: "pointer" });
            let labelContent;
            if (pColor.valor === 'all') {
                labelContent = pColor.nome;
                checkbox.addEventListener('change', () => {
                    if (checkbox.checked) {
                        selectedColors.add('all');
                        colorCheckboxes.forEach(cb => { cb.checked = true; if (!selectedColors.has(cb.dataset.rgb)) selectedColors.add(cb.dataset.rgb); });
                    } else {
                        selectedColors.clear();
                        colorCheckboxes.forEach(cb => { cb.checked = false; });
                    }
                });
                Object.assign(masterCheckboxContainer, checkbox);
            } else {
                const colorBox = document.createElement("div");
                Object.assign(colorBox.style, { width: "20px", height: "20px", backgroundColor: pColor.rgb, border: "1px solid #fff5", marginRight: "8px", borderRadius: "4px" });
                entryDiv.appendChild(colorBox);
                labelContent = pColor.nome;
                checkbox.dataset.rgb = pColor.rgb;
                colorCheckboxes.push(checkbox);
                checkbox.addEventListener('change', () => {
                    if (checkbox.checked) { selectedColors.add(pColor.rgb); }
                    else { selectedColors.delete(pColor.rgb); selectedColors.delete('all'); }
                    masterCheckboxContainer.checked = colorCheckboxes.every(cb => cb.checked);
                    if(masterCheckboxContainer.checked) selectedColors.add('all');
                });
            }
            const label = document.createElement("label");
            label.htmlFor = `color-checkbox-${index}`; label.textContent = labelContent;
            Object.assign(label.style, { cursor: "pointer", flexGrow: "1", fontSize: "12px" });
            entryDiv.appendChild(checkbox);
            entryDiv.appendChild(label);
            if(pColor.valor !== 'all') {
                const counterSpan = document.createElement("span");
                counterSpan.id = `counter-span-${pColor.rgb}`; counterSpan.textContent = "Faltam: 0";
                Object.assign(counterSpan.style, { marginLeft: "10px", backgroundColor: "#ffffff22", padding: "2px 6px", borderRadius: "4px", fontSize: "12px" });
                entryDiv.appendChild(counterSpan);
            }
            content.appendChild(entryDiv);
        });
        sidebar.appendChild(content);
        document.body.appendChild(sidebar);
    }

    function updateCountersUI() {
        let totalWrong = 0, totalPixels = 0;
        for (const rgb in colorCounters) {
            const data = colorCounters[rgb];
            const span = document.getElementById(`counter-span-${rgb}`);
            if (span) { span.textContent = `Faltam: ${data.wrongPixelsInChunk || 0}`; }
            if (selectedColors.has('all') || selectedColors.has(rgb)) {
                totalWrong += data.wrongPixelsInChunk || 0;
                totalPixels += data.totalPixelsInChunk || 0;
            }
        }
        pixelCounter.textContent = `Total selecionado: ${totalWrong}`;
        const percentage = totalPixels === 0 ? 100 : (((totalPixels - totalWrong) / totalPixels) * 100).toFixed(2).replace(".", ",");
        percentageCounter.textContent = `Progresso: ${percentage}%`;
    }

    function patchModeButtonUI() {
        if (document.getElementById("overlay-mode-button")) return;
        let modeButton = document.createElement("button");
        modeButton.id = "overlay-mode-button";
        modeButton.textContent = "Modo: " + overlayMode.charAt(0).toUpperCase() + overlayMode.slice(1);
        Object.assign(modeButton.style, { backgroundColor: "#0e0e0e7f", color: "white", border: "solid", borderColor: "#1d1d1d7f", borderRadius: "4px", padding: "5px 10px", cursor: "pointer", backdropFilter: "blur(2px)" });
        modeButton.addEventListener("click", () => {
            overlayMode = OVERLAY_MODES[(OVERLAY_MODES.indexOf(overlayMode) + 1) % OVERLAY_MODES.length];
            modeButton.textContent = "Modo: " + overlayMode.charAt(0).toUpperCase() + overlayMode.slice(1);
        });
        const buttonContainer = document.querySelector("div.gap-4:nth-child(1) > div:nth-child(2)");
        if (buttonContainer) {
            buttonContainer.appendChild(modeButton);
            buttonContainer.classList.remove("items-center");
            buttonContainer.classList.add("items-end");
        }
    }

    // --- Funções Auxiliares ---
    async function loadImage(src) { return new Promise((resolve) => { const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => resolve({ img, width: img.naturalWidth, height: img.naturalHeight }); img.src = src; }); }
    async function blobToImage(blob) { return new Promise((resolve) => { const img = new Image(); img.onload = () => resolve(img); img.src = URL.createObjectURL(blob); }); }

    const observer = new MutationObserver(() => {
        patchModeButtonUI();
    });
    const interval = setInterval(() => {
        const targetNode = document.querySelector("div.gap-4:nth-child(1)");
        if (targetNode) {
            clearInterval(interval);
            observer.observe(targetNode, { childList: true, subtree: true });
            patchModeButtonUI();
        }
    }, 500);

})();