MCSR Ranked Share Button

Allows sharing a wordle-style overview of each match.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
 // ==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();

})();