Reorder Certificates

Reorders certificates based on completion percentage and simplifies the interface

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Reorder Certificates
// @namespace    FarmRPGCertificates
// @version      1.1.1
// @description  Reorders certificates based on completion percentage and simplifies the interface
// @author       ClientCoin
// @match        http*://*farmrpg.com/index.php
// @match        http*://*farmrpg.com/
// @match        http*://*alpha.farmrpg.com/
// @match        http*://*alpha.farmrpg.com/index.php
// @icon         https://www.google.com/s2/favicons?sz=64&domain=farmrpg.com
// @grant        none
// ==/UserScript==



function reorderCertificates() {

    const locName = location.hash.slice(location.hash.search(/[^#!\/]/), location.hash.search(/.php/))
    if (locName != "temple") {
        return;
    }

    //console.log("%c[Script] Reordering certificates...", "color: cyan; font-weight: bold;");

    let allContentBlocks = document.querySelectorAll(".content-block");
    let certificatesContainer = null;

    allContentBlocks.forEach(block => {
        let titleElement = block.querySelector(".content-block-title");
        if (titleElement && titleElement.textContent.trim() === "Secret Chains") {
            certificatesContainer = block.querySelector(".list-block ul");
        }
    });

    if (!certificatesContainer) {
        console.warn("%c[Script] Certificates container not found. Exiting.", "color: red;");
        return;
    }

    let certificateElements = Array.from(certificatesContainer.querySelectorAll("li"));
    if (certificateElements.length === 0) {
        console.warn("%c[Script] No certificate elements found. Exiting.", "color: red;");
        return;
    }

    //console.log("%c[Script] Found certificate elements:", "color: cyan;", certificateElements);

    let certificateBlocks = certificateElements.map(certificate => {
        let progress = extractProgress(certificate);
        //console.log(`%c[Script] Extracted progress: ${progress}%`, "color: lightgreen;");
        return { element: certificate, progress };
    });

    let completedCount = certificateBlocks.filter(c => c.progress === 100).length;
    let maxCount = certificateBlocks.length;

    // Sorting: Completed (100%) certificates at the bottom
    certificateBlocks.sort((a, b) => {
        // Move 100% completed items to the bottom
        if (a.progress === 100 && b.progress !== 100) return 1;
        if (b.progress === 100 && a.progress !== 100) return -1;

        // Sort by highest progress first
        let progressSort = b.progress - a.progress;
        if (progressSort !== 0) return progressSort;

        // If progress is the same, sort by itemsToGo (ascending)
        return a.itemsToGo - b.itemsToGo;
    });



    //console.log("%c[Script] Certificates sorted by progress.", "color: cyan;");

    certificatesContainer.innerHTML = ""; // Clear the list

    // Insert summary at the top
    let summaryElement = document.createElement("li");
    const certificateImages = [
        '7896-yellow.png', '7896-yellow.png', '7896-yellow.png',
        '7896-yellow.png', '7896-yellow.png', '7896-yellow.png',
        '7896-green.png', '7896-green.png', '7896-green.png',
        '7896-green.png', '7896-green.png', '7896-green.png',
        '7896.png', '7896.png', '7896.png', '7896.png',
        '7896-orange.png', '7896-orange.png',
        '7896-purple.png', '7896-purple.png',
        '7896-gray.png',
        '7896-blue.png'
    ];

    // Shuffle function
    function shuffleArray(arr) {
        return arr.map(value => ({ value, sort: Math.random() }))
            .sort((a, b) => a.sort - b.sort)
            .map(({ value }) => value);
    }

    const shuffledCertificates = shuffleArray(certificateImages);
    const beforeImages = shuffledCertificates.slice(0, 11);
    const afterImages = shuffledCertificates.slice(11);

    // Create HTML blocks
    const makeImageBlock = (images) =>
    images.map(src => `<img src="/img/items/${src}" style="height:1rem;width:1rem;margin:0 1px;">`).join('');

    summaryElement.innerHTML = `
    <div style="display: flex; align-items: center; justify-content: center; gap: 4px; white-space: nowrap; overflow: hidden;">
        <div style="display: flex; gap: 2px; flex-shrink: 1; overflow: hidden;">
            ${makeImageBlock(beforeImages)}
        </div>
        <div style="font-weight: bold; color: gold; text-align: center; padding: 5px; flex-shrink: 0; min-width: 180px;">
            ${completedCount}/${maxCount} Certificates COMPLETE
        </div>
        <div style="display: flex; gap: 2px; flex-shrink: 1; overflow: hidden;">
            ${makeImageBlock(afterImages)}
        </div>
    </div>
`;

    certificatesContainer.appendChild(summaryElement);

// === Progress Summary (Total Crops Remaining) ===
// === Progress Summary (Total Crops Remaining) ===
let totalToGo = 0;
let cropCount = 0;

certificateBlocks.forEach(({ element }) => {
    const span = element.querySelector(".item-inner span");
    if (!span) return;

    const rawText = span.textContent.trim();
    //console.log("[DEBUG] Raw span content:", rawText);

    const match = rawText.match(/([\d,]+)\s*\/\s*([\d,]+)\s*Items Sacrificed/i);
    if (match) {
        const sacrificed = parseInt(match[1].replace(/,/g, ''));
        const required = parseInt(match[2].replace(/,/g, ''));
        const toGo = required - sacrificed;
        totalToGo += toGo;
        cropCount++;
        //console.log(`[DEBUG] Sacrificed=${sacrificed}, Required=${required}, To Go=${toGo}`);
    } else if (/COMPLETE/i.test(rawText)) {
        cropCount++;
        //console.log("[DEBUG] COMPLETE certificate detected");
    } else {
        console.warn("[DEBUG] No match for sacrificed/required in:", rawText);
    }
});

const totalRequired = cropCount * 100000;
const percentComplete = ((totalRequired - totalToGo) / totalRequired * 100).toFixed(2);

const cropSummary = document.createElement("li");
cropSummary.innerHTML = `
    <div style="font-weight: bold; color: lightgreen; text-align: center; padding: 5px;">
        ${totalToGo.toLocaleString()} to go out of ${totalRequired.toLocaleString()} crops (${percentComplete}%)
    </div>
`;
certificatesContainer.appendChild(cropSummary);





    // Append reordered certificates
    certificateBlocks.forEach(({ element }) => {
        simplifyText(element);
        certificatesContainer.appendChild(element);
    });
    console.log("Certificates reordered and updated.");
}

function extractProgress(element) {
    let titleElement = element.querySelector(".item-title");
    if (!titleElement) {
        console.warn("%c[Script] Title element not found in certificate block.", "color: orange;");
        return 0;
    }

    // Extract sacrificed and required item counts
    let progressMatch = titleElement.innerText.match(/(\d{1,3}(?:,\d{3})*) \/ (\d{1,3}(?:,\d{3})*) Items Sacrificed/);

    if (!progressMatch) {
        console.warn("%c[Script] No progress values found in:", "color: orange;", titleElement.innerText);
        return 0;
    }

    let sacrificed = parseInt(progressMatch[1].replace(/,/g, '')); // Remove commas
    let required = parseInt(progressMatch[2].replace(/,/g, ''));

    // Calculate precise percentage
    let progress = (sacrificed / required) * 100;

    //console.log(`%c[Script] Corrected progress: ${progress.toFixed(2)}% (${sacrificed}/${required})`, "color: lightgreen;");

    return progress;
}


function simplifyText(element) {
    let titleElement = element.querySelector(".item-title");
    let imgElement = element.querySelector(".item-media img");

    if (!titleElement) {
        console.warn("%c[Script] Title element not found in certificate block.", "color: orange;");
        return;
    }

    if (element.classList.contains("row")) {
        element.style.marginBottom = "0px";
    }

    let nameMatch = titleElement.innerText.match(/Certificate of (.+?) Giving/);
    let progressMatch = titleElement.innerText.match(/(\d+(?:,\d+)?) \/ (\d+(?:,\d+)?) Items Sacrificed/);
    let onHandMatch = titleElement.innerText.match(/Sacrifice: (.+?) \(You have (\d+(?:,\d+)?)\)/);

    if (nameMatch && progressMatch && onHandMatch) {
        let itemName = nameMatch[1];
        let totalRequired = parseInt(progressMatch[2].replace(/,/g, ''));
        let sacrificed = parseInt(progressMatch[1].replace(/,/g, ''));
        let onHand = parseInt(onHandMatch[2].replace(/,/g, ''));
        let itemsToGo = totalRequired - sacrificed;
        let progressPercent = (sacrificed / totalRequired) * 100;

        let color = "white";
        if (itemsToGo === 0) color = "gold";
        else if (progressPercent >= 75) color = "lightgreen";
        else if (sacrificed < 10000) color = "grey";

        let lower10kThreshold = Math.floor(itemsToGo / 10000) * 10000;
        let remainingToLower10k = itemsToGo - lower10kThreshold;
        if (remainingToLower10k === 0) remainingToLower10k = 10000;

        let nn = "COMPLETE".padStart(15, '\u00A0');
        let outputPad = itemsToGo === 0
        ? `${nn} [${onHand.toLocaleString().padStart(7, '\u00A0')} on hand]`
    : `${itemsToGo.toLocaleString().padStart(9, '\u00A0')} to go [${onHand.toLocaleString().padStart(7, '\u00A0')} on hand]`;


        // Build the left block (60%) — content inside <a>
        let container = document.createElement("div");
        container.style.display = "flex";
        container.style.flexFlow = "row nowrap";
        container.style.alignItems = "center";
        container.style.verticalAlign = "middle";
        container.style.marginTop = "auto";
        container.style.marginBottom = "auto";

        let left = document.createElement("div");
        left.style.width = "20%";
        left.style.textAlign = "left";
        left.textContent = `${itemName}: `;

        let right = document.createElement("div");
        right.style.width = "80%";
        right.style.textAlign = "left";
        right.style.whiteSpace = "nowrap";
        right.style.overflow = "hidden";
        right.style.textOverflow = "ellipsis"; // optional, if you want to clip long lines with …
        right.innerHTML = `${outputPad}`;


        container.appendChild(left);
        container.appendChild(right);

        let span = document.createElement("span");
        span.style.color = color;
        span.appendChild(container);

        let outer = document.createElement("span");
        outer.style.fontFamily = "monospace";
        outer.style.width = "100%";
        outer.style.fontSize = "0.833rem";
        outer.appendChild(span);

        titleElement.outerHTML = outer.outerHTML;

        if (imgElement) {
            imgElement.style.width = "1rem";
            imgElement.style.height = "1rem";
        }

        let smallImgElement = element.querySelector(".item-after img");
        if (smallImgElement) {
            smallImgElement.style.width = "1rem";
            smallImgElement.style.height = "1rem";
        }

        // === Restructure LI ===
        let liElement = element.closest("li");
        let aTag = liElement.querySelector("a");
        if (!liElement || !aTag) return;

        // Create flex container
        let wrapper = document.createElement("div");
        wrapper.style.display = "flex";
        wrapper.style.width = "100%";

        // Left side: wrap <a> in div
        let leftDiv = document.createElement("div");
        leftDiv.style.width = "60%";
        leftDiv.appendChild(aTag); // Moves <a> inside left div

        // Right side: build button panel
        let rightDiv = document.createElement("div");
        rightDiv.style.width = "40%";
        rightDiv.style.display = "flex";
        rightDiv.style.flexDirection = "row";
        rightDiv.style.alignItems = "center";
        rightDiv.style.justifyContent = "space-between";

        const wrap = (link) => {
            const wrapper = document.createElement("div");
            wrapper.style.width = "33.33%";
            wrapper.style.display = "flex";
            wrapper.style.justifyContent = "center";
            wrapper.appendChild(link);
            return wrapper;
        };

        const isComplete = itemsToGo === 0;
        const notEnoughForNext10k = onHand < remainingToLower10k;

        const link1 = createLink(`Give ${onHand}`, onHand, itemName, 'all', itemsToGo, remainingToLower10k, isComplete);
        const give1kDisabled = isComplete || onHand < 1000;
        const link2 = createLink(`Give 1k`, onHand, itemName, '1k', itemsToGo, remainingToLower10k, give1kDisabled);
        const link3 = createLink(`Give ${remainingToLower10k}`, onHand, itemName, 'tothenext10k', itemsToGo, remainingToLower10k, isComplete || notEnoughForNext10k);


        rightDiv.appendChild(wrap(link1));
        rightDiv.appendChild(wrap(link2));
        rightDiv.appendChild(wrap(link3));


        // Clear the original LI content and re-append
        liElement.innerHTML = "";
        wrapper.appendChild(leftDiv);
        wrapper.appendChild(rightDiv);
        liElement.appendChild(wrapper);
    } else {
        console.warn("%c[Script] Failed to extract data for certificate text update.", "color: orange;");
    }
}

function createLink(linkText, onHand, itemName, type, itemsToGo, remainingToLower10k, disabled = false) {
    let link = document.createElement("a");
    link.innerText = linkText;
    link.href = "#";

    link.style.textDecoration = "none";
    link.style.background = "transparent";
    link.style.border = "1px solid";
    link.style.borderRadius = "6px";
    link.style.fontSize = "0.7rem";
    link.style.cursor = disabled ? "not-allowed" : "pointer";
    link.style.padding = "0.1rem 0.6rem";
    link.style.margin = "0";
    link.style.display = "inline-block";
    link.style.width = "90px";               // fixed width
    link.style.textAlign = "center";
    link.style.zIndex = 2;
    link.style.whiteSpace = "nowrap";

    if (disabled) {
        link.style.color = "gray";
        link.style.borderColor = "gray";
        link.onclick = function (e) {
            e.preventDefault();
        };
    } else {
        link.style.color = "gold";
        link.style.borderColor = "gold";

        link.onclick = function (e) {
            e.preventDefault();
            const params = new URLSearchParams();
            params.append("go", "sacrificeitem");
            params.append("item", itemName);
            params.append("amt", getAmount(type, onHand, itemsToGo, remainingToLower10k));
            params.append("special", "1");

            const url = `worker.php?${params.toString()}`;
            fetch(url, { method: "GET" })
                .then((response) => response.text())
                .then((data) => {
                if (data === "success") {
                    window.location.reload();
                } else {
                    console.warn("%c[Error] Something went wrong: ", "color: red;", data);
                }
            })
                .catch((error) => {
                console.error("%c[Error] AJAX request failed: ", "color: red;", error);
            });
        };
    }

    return link;
}


function getAmount(type, onHand, itemsToGo, remainingToLower10k) {
    switch (type) {
        case "all":
            return onHand;
        case "1k":
            return 1000;
        case "tothenext10k":
            return remainingToLower10k;
        default:
            return 0;
    }
}








function init() {
    //console.log("%c[Script] Initializing...", "color: green;");
    reorderCertificates();
    injectCSS();
}

window.addEventListener("load", init); // Ensures the script runs only once


function injectCSS() {
    const style = document.createElement('style');
    style.innerHTML = `
.list-block .item-content {
    min-height: 0 !important;
}

.list-block .item-inner {
    min-height: 0 !important;
}



    `;
    document.head.appendChild(style);
}

$(document).ready( () => {
    const target = document.querySelector("#fireworks")
    const observer = new MutationObserver( mutation => {if (mutation[0]?.attributeName == "data-page") reorderCertificates()} )


    const config = {
        attributes: true,
        childlist: true,
        subtree: true
    }


    observer.observe(target, config);

    const observera = new MutationObserver(() => {
        reorderCertificates();
    });

})