Allows sharing a wordle-style overview of each match.
// ==UserScript==
// @name MCSR Ranked Share Button
// @namespace http://tampermonkey.net/
// @version 1.01
// @description Allows sharing a wordle-style overview of each match.
// @author ffz
// @match https://mcsrranked.com/*
// @grant none
// ==/UserScript==
(function() {
"use strict";
// helpers
const pad = (value, length) => {
return String(value).padEnd(length, " ");
};
const fmt = (ms) => {
if (ms == null) return "—";
const totalSeconds = Math.floor(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes}:${String(seconds).padStart(2, "0")}`;
};
const capitalizeFirst = (text) => {
return text.charAt(0) + text.slice(1).toLowerCase();
};
const seedLabel = (text) => {
return capitalizeFirst(text.replaceAll("_", " ").toLowerCase());
};
// getting timelines
function getTime(timelines, playerId, eventType) {
const event = timelines.find((entry) => {
return entry.uuid === playerId && entry.type === eventType;
});
return event?.time ?? null;
}
function buildTimeline(matchData, playerId) {
const timelines = matchData.timelines;
return {
nether: getTime(timelines, playerId, "story.enter_the_nether"),
bastion: getTime(timelines, playerId, "nether.find_bastion"),
fortress: getTime(timelines, playerId, "nether.find_fortress"),
blind: getTime(timelines, playerId, "projectelo.timeline.blind_travel"),
stronghold: getTime(timelines, playerId, "story.follow_ender_eye"),
endEnter: getTime(timelines, playerId, "story.enter_the_end"),
finish: matchData.result?.uuid === playerId ? matchData.result.time : null
};
}
// build share text
function buildShare(matchData, formatted = true) {
let playerA = matchData.players[0];
let playerB = matchData.players[1];
const winnerId = matchData.result?.uuid;
if (playerB.uuid === winnerId) {
[playerA, playerB] = [playerB, playerA];
}
const timelineA = buildTimeline(matchData, playerA.uuid);
const timelineB = buildTimeline(matchData, playerB.uuid);
const wasForfeited = matchData.forfeited;
let medalA = "🏆";
let medalB = "🥈";
if (wasForfeited) {
if (winnerId === playerA.uuid) {
medalA = "🏆";
medalB = "☠️";
} else {
medalA = "☠️";
medalB = "🏆";
}
}
const overworld = seedLabel(matchData.seedType);
const bastionType = seedLabel(matchData.bastionType);
const leftWidth = 6;
const midWidth = 18;
const formatRow = (left, label, right, isBold = false) => {
const line = `${pad(fmt(left), leftWidth)} ${pad(label, midWidth)} ${fmt(right)}`;
return formatted && isBold ? `**${line}**` : line;
};
const rows = [
["nether", "🟥 Nether 🌋"],
["bastion", "⬛ Bastion 🏰"],
["fortress", "🟧 Fortress 🔥"],
["blind", "🟪 Blind 🔮"],
["stronghold", "⬜ Stronghold ⛓️"],
["endEnter", "🟨 Enter End 🌀"],
["finish", "🟩 Finish 🐉", true]
];
const title = formatted
? `[MCSR Ranked — Match #${matchData.id}](${window.location.href})`
: `MCSR Ranked — Match #${matchData.id}`;
const subtitle = formatted
? `*${overworld} / ${bastionType} bastion*`
: `${overworld} / ${bastionType} bastion`;
const lines = [
title,
subtitle,
"",
`${medalA} ${playerA.nickname} vs ${playerB.nickname} ${medalB}`,
""
];
for (const [key, label, isBold = false] of rows) {
lines.push(formatRow(timelineA[key], label, timelineB[key], isBold));
}
const scriptLine = formatted
? "-# install a userscript manager (e.g. [tampermonkey](<https://chromewebstore.google.com/detail/dhdgffkkebhmkfjojejmpbldmpobfkfo>)) then install the [script](<https://greasyfork.org/en/scripts/566713-mcsr-ranked-share-button>). made by ffz"
: "https://greasyfork.org/en/scripts/566713-mcsr-ranked-share-button";
lines.push("");
lines.push(scriptLine);
return lines.join("\n");
}
// api fetch
async function fetchMatch() {
const pathParts = location.pathname.split("/");
const matchId = pathParts.pop();
if (!matchId) return null;
const apiUrl = `https://mcsrranked.com/api/matches/${matchId}`;
const response = await fetch(apiUrl);
const json = await response.json();
return json.data;
}
// mount button
function mountBtn(node) {
const tryMountButton = () => {
const vsLink = document.querySelector('a[href*="/vs/"]');
if (!vsLink) return false;
const container = vsLink.parentElement || vsLink.closest("div.flex");
if (!container) return false;
const needsMove = node.parentElement !== container || node.nextSibling !== vsLink;
if (needsMove) container.insertBefore(node, vsLink);
return true;
};
setInterval(tryMountButton, 1000);
const observer = new MutationObserver(() => {
tryMountButton();
});
observer.observe(document.body, { childList: true, subtree: true });
}
// button
function createBtn(label) {
const button = document.createElement("button");
button.textContent = label;
button.style.height = "36px";
button.style.padding = "0 14px";
button.style.borderRadius = "999px";
button.style.marginLeft = "8px";
button.style.background = "rgba(255,255,255,0.06)";
button.style.color = "#e4e4e7";
button.style.border = "1px solid rgba(255,255,255,0.15)";
button.style.fontWeight = "600";
button.style.cursor = "pointer";
button.style.transition = "all .15s ease";
button.onmouseenter = () => {
button.style.background = "rgba(255,255,255,0.12)";
};
button.onmouseleave = () => {
button.style.background = "rgba(255,255,255,0.06)";
};
return button;
}
// init
function init() {
const container = document.createElement("span");
container.style.display = "inline-flex";
container.style.gap = "6px";
container.style.position = "relative";
container.style.paddingRight = "4px";
const handleShare = async (formatted) => {
const matchData = await fetchMatch();
const text = buildShare(matchData, formatted);
await navigator.clipboard.writeText(text);
};
const runShare = async (button, label, formatted) => {
try {
button.textContent = "...";
await handleShare(formatted);
button.textContent = "Copied!";
setTimeout(() => {
button.textContent = label;
}, 2000);
} catch (error) {
console.error(error);
button.textContent = "Error";
setTimeout(() => {
button.textContent = label;
}, 2000);
}
};
const shareButton = createBtn("Share");
const menu = document.createElement("div");
menu.style.position = "absolute";
menu.style.top = "calc(100% + 6px)";
menu.style.right = "0";
menu.style.padding = "4px";
menu.style.background = "rgba(20,20,24,0.98)";
menu.style.border = "1px solid rgba(255,255,255,0.12)";
menu.style.borderRadius = "10px";
menu.style.zIndex = "9999";
menu.style.boxShadow = "0 10px 30px rgba(0,0,0,0.35)";
menu.style.minWidth = "max-content";
menu.style.whiteSpace = "nowrap";
menu.style.flexDirection = "column";
menu.style.display = "none";
const makeMenuBtn = (label, formatted) => {
const btn = document.createElement("button");
btn.textContent = label;
btn.style.padding = "8px 12px";
btn.style.borderRadius = "8px";
btn.style.background = "rgba(255,255,255,0.06)";
btn.style.color = "#e4e4e7";
btn.style.border = "1px solid rgba(255,255,255,0.12)";
btn.style.fontWeight = "600";
btn.style.cursor = "pointer";
btn.style.transition = "all .15s ease";
btn.style.margin = "2px";
btn.style.width = "100%";
btn.style.textAlign = "left";
btn.onmouseenter = () => {
btn.style.background = "rgba(255,255,255,0.12)";
};
btn.onmouseleave = () => {
btn.style.background = "rgba(255,255,255,0.06)";
};
btn.onclick = async (event) => {
event.stopPropagation();
menu.style.display = "none";
await runShare(shareButton, "Share", formatted);
};
return btn;
};
const markdownBtn = makeMenuBtn("Discord Format", true);
const plainBtn = makeMenuBtn("Unformatted", false);
menu.append(markdownBtn, plainBtn);
shareButton.onclick = (event) => {
event.stopPropagation();
menu.style.display = menu.style.display === "none" ? "flex" : "none";
};
document.addEventListener("click", () => {
menu.style.display = "none";
});
container.append(shareButton, menu);
mountBtn(container);
}
init();
})();