Overlay da Missão com Contadores

Overlay da Missão para WPlace com contadores

// ==UserScript==
// @name         Overlay da Missão com Contadores
// @namespace    http://tampermonkey.net/
// @version      2.1.21
// @description  Overlay da Missão para WPlace com contadores
// @author       Víkish
// @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';
const CHUNK_WIDTH = 1000;
const CHUNK_HEIGHT = 1000;
const overlays = await fetchData();
for (const obj of overlays) {
    obj.chunksString = `/${obj.chunk[0]}/${obj.chunk[1]}.png`;
    const { img, width, height } = await loadImage(obj.url);
    const overlayCanvas = new OffscreenCanvas(1000, 1000);
    const overlayCtx = overlayCanvas.getContext("2d");
    overlayCtx.drawImage(img, obj.coords[0], obj.coords[1], width, height);
    obj.imageData = overlayCtx.getImageData(0, 0, 1000, 1000);
}
const OVERLAY_MODES = ["overlay", "original", "chunks"];
let overlayMode = OVERLAY_MODES[0];
// Criação do contêiner dos contadores (mantém-se centralizado no topo)
const counterContainer = document.createElement("div");
counterContainer.id = "pixel-counter";
Object.assign(counterContainer.style, {
    position: "fixed",
    top: "5px",
    left: "50%",
    transform: "translateX(-50%)",
    zIndex: "10000",
    padding: "6px 10px",
    fontSize: "12px",
    fontFamily: "Arial, sans-serif",
    backgroundColor: "rgba(0,0,0,0.66)",
    color: "white",
    borderRadius: "6px",
    pointerEvents: "none",
    backdropFilter: "blur(3px)",
    lineHeight: "1.25",
    textAlign: "center"
});
document.body.appendChild(counterContainer);
const pixelCounter = document.createElement("div");
pixelCounter.textContent = "Pixeis restantes: 0";
counterContainer.appendChild(pixelCounter);
const percentageCounter = document.createElement("div");
percentageCounter.textContent = "Progresso atual: 0,00%";
counterContainer.appendChild(percentageCounter);
// === TABELA DE CORES (no canto superior esquerdo, um pouco abaixo do topo) ===
const colorStatsContainer = document.createElement("div");
Object.assign(colorStatsContainer.style, {
    position: "fixed",
    top: "170px",      // um pouco abaixo do topo para não sobrepor os botões do site
    left: "10px",
    backgroundColor: "rgba(0,0,0,0.8)",
    color: "white",
    fontSize: "11px",
    padding: "6px",
    borderRadius: "8px",
    zIndex: "10000",
    maxHeight: "320px",
    overflowY: "auto",
    minWidth: "25px",
    maxWidth: "150px",
    boxSizing: "border-box"
});
// Toggle: seta para cima = ocultar, seta para baixo = mostrar
const toggleButton = document.createElement("div");
toggleButton.textContent = "↑"; // inicialmente visível -> seta para cima (ocultar)
Object.assign(toggleButton.style, {
    cursor: "pointer",
    textAlign: "center",
    marginBottom: "6px",
    fontSize: "14px",
    userSelect: "none",
    pointerEvents: "auto"
});
const colorList = document.createElement("div");
colorList.style.display = "block";
colorStatsContainer.appendChild(toggleButton);
colorStatsContainer.appendChild(colorList);
document.body.appendChild(colorStatsContainer);
// Mantém a largura do container mesmo quando escondido
let listVisible = true;
toggleButton.addEventListener("click", () => {
    listVisible = !listVisible;
    colorList.style.display = listVisible ? "block" : "none";
    toggleButton.textContent = listVisible ? "↑" : "↓";
    // não altera a largura do colorStatsContainer para evitar "encolher" ao esconder
});
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) {
            throw new Error("Invalid URL provided to fetch");
        }
        if (overlayMode === "overlay") {
            if (url.hostname === "backend.wplace.live" && url.pathname.startsWith("/files/")) {
                for (const obj of overlays) {
                    if (url.pathname.endsWith(obj.chunksString)) {
                        const originalResponse = await target.apply(thisArg, argList);
                        const originalBlob = await originalResponse.blob();
                        const originalImage = await blobToImage(originalBlob);
                        const width = originalImage.width;
                        const height = originalImage.height;
                        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 d1 = originalData.data;
                        const d2 = obj.imageData.data;
                        const dr = resultData.data;
                        let wrongPixels = 0;
                        let totalTargetPixels = 0;
                        const localColorCount = {};
                        for (let i = 0; i < d1.length; i += 4) {
                            const isTransparent = d2[i + 3] === 0;
                            if (!isTransparent) totalTargetPixels++;
                            const samePixel =
                                d1[i] === d2[i] &&
                                d1[i + 1] === d2[i + 1] &&
                                d1[i + 2] === d2[i + 2] &&
                                d1[i + 3] === d2[i + 3];
                            if (!samePixel && !isTransparent) {
                                wrongPixels++;
                                const key = `${d2[i]},${d2[i + 1]},${d2[i + 2]}`;
                                localColorCount[key] = (localColorCount[key] || 0) + 1;
                            }
                            if (samePixel && !isTransparent) {
                                dr[i] = 0;
                                dr[i + 1] = 255;
                                dr[i + 2] = 0;
                                dr[i + 3] = 255;
                            } else if (!isTransparent) {
                                dr[i] = d2[i];
                                dr[i + 1] = d2[i + 1];
                                dr[i + 2] = d2[i + 2];
                                dr[i + 3] = d2[i + 3];
                            }
                        }
                        // Atualiza os contadores no topo
                        pixelCounter.textContent = `Pixeis restantes: ${wrongPixels}`;
                        const percentage = totalTargetPixels === 0 ? "100,00" :
                            (((totalTargetPixels - wrongPixels) / totalTargetPixels) * 100).toFixed(2).replace(".", ",");
                        percentageCounter.textContent = `Progresso atual: ${percentage}%`;
                        // Atualiza a lista de cores (ordenada descrescente, remove zeros)
                        colorList.innerHTML = "";
                        const sorted = Object.entries(localColorCount)
                            .filter(([,cnt]) => cnt > 0)
                            .sort((a, b) => b[1] - a[1]);
                        for (const [key, count] of sorted) {
                            const [r, g, b] = key.split(",").map(Number);
                            const item = document.createElement("div");
                            item.style.display = "flex";
                            item.style.alignItems = "center";
                            item.style.marginBottom = "4px";
                            item.style.pointerEvents = "none";
                            const colorSquare = document.createElement("div");
                            colorSquare.style.width = "12px";
                            colorSquare.style.height = "12px";
                            colorSquare.style.marginRight = "6px";
                            colorSquare.style.backgroundColor = `rgb(${r}, ${g}, ${b})`;
                            colorSquare.style.borderRadius = "2px";
                            const label = document.createElement("span");
                            label.textContent = `${count}`;
                            label.style.whiteSpace = "nowrap";
                            item.appendChild(colorSquare);
                            item.appendChild(label);
                            colorList.appendChild(item);
                        }
                        ctx.putImageData(resultData, 0, 0);
                        const mergedBlob = await canvas.convertToBlob();
                        return new Response(mergedBlob, {
                            headers: { "Content-Type": "image/png" }
                        });
                    }
                }
            }
        } else if (overlayMode === "chunks") {
            if (url.hostname === "backend.wplace.live" && url.pathname.startsWith("/files/")) {
                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", { willReadFrequently: true });
                ctx.strokeStyle = 'red';
                ctx.lineWidth = 1;
                ctx.strokeRect(0, 0, CHUNK_WIDTH, CHUNK_HEIGHT);
                ctx.font = '30px 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);
    }
});
async function fetchData() {
    const response = await fetch("https://gist.githubusercontent.com/yl99a/45ec3df57cc75c4b93c45251b87eb20b/raw/overlays.json?" + Date.now());
    return await response.json();
}
function blobToImage(blob) {
    return new Promise((resolve, reject) => {
        const img = new Image();
        img.onload = () => resolve(img);
        img.onerror = reject;
        img.src = URL.createObjectURL(blob);
    });
}
function loadImage(src) {
    return new Promise((resolve, reject) => {
        const img = new Image();
        img.crossOrigin = "anonymous";
        img.onload = () => {
            resolve({
                img,
                width: img.naturalWidth,
                height: img.naturalHeight
            });
        };
        img.onerror = reject;
        img.src = src;
    });
}
function patchUI() {
    if (document.getElementById("overlay-blend-button")) return;
    let blendButton = document.createElement("button");
    blendButton.id = "overlay-blend-button";
    blendButton.textContent = overlayMode.charAt(0).toUpperCase() + overlayMode.slice(1);
    blendButton.style.backgroundColor = "#0e0e0e7f";
    blendButton.style.color = "white";
    blendButton.style.border = "solid";
    blendButton.style.borderColor = "#1d1d1d7f";
    blendButton.style.borderRadius = "4px";
    blendButton.style.padding = "5px 10px";
    blendButton.style.cursor = "pointer";
    blendButton.style.backdropFilter = "blur(2px)";
    blendButton.addEventListener("click", () => {
        overlayMode = OVERLAY_MODES[(OVERLAY_MODES.indexOf(overlayMode) + 1) % OVERLAY_MODES.length];
        blendButton.textContent = overlayMode.charAt(0).toUpperCase() + overlayMode.slice(1);
    });
    const buttonContainer = document.querySelector("div.gap-4:nth-child(1) > div:nth-child(2)");
    const leftSidebar = document.querySelector("html body div div.disable-pinch-zoom.relative.h-full.overflow-hidden.svelte-6wmtgk div.absolute.right-2.top-2.z-30 div.flex.flex-col.gap-4.items-center");
    if (buttonContainer) {
        buttonContainer.appendChild(blendButton);
        buttonContainer.classList.remove("items-center");
        buttonContainer.classList.add("items-end");
    }
    if (leftSidebar) {
        leftSidebar.classList.add("items-end");
        leftSidebar.classList.remove("items-center");
    }
}
const observer = new MutationObserver(() => {
    patchUI();
});
observer.observe(document.querySelector("div.gap-4:nth-child(1)"), {
    childList: true,
    subtree: true
});
patchUI();
})();