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