// ==UserScript==
// @name Reorder Certificates
// @namespace FarmRPGCertificates
// @version 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();
});
})