Greasy Fork is available in English.
Real-time gathering, refining, and XP/hour calculator for Idle Artisan.
// ==UserScript==
// @name Idle Artisan Tracker Ultimate Mozilla Script
// @namespace https://greasyfork.org/users/Kota
// @version 1.1
// @description Real-time gathering, refining, and XP/hour calculator for Idle Artisan.
// @author Nightchildkota
// @match *://*.idleartisan.com/*
// @license MIT
// @grant none
// @run-at document-end
// ==/UserScript==
(function () {
const SECONDS_PER_ACTION = 2;
let lastResourceSnapshot = {};
let lastGatherRates = {};
let inactivityTicks = 0;
// --- XP tracking ---
let lastXPPerHour = 0;
let lastSkill = null;
let ignoreNextXP = false;
// --- Skill/Resource Mappings ---
const GATHER_SKILL_MAP = {
"Mining": "Iron Ore",
"Woodcutting": "Wood",
"Fishing": "Fish",
"Hunting": "Carcass",
"Battling": "Gold",
"Scavenging": "Salvage"
};
const REFINE_INPUT_MAP = {
"Gold": "Gold",
"Iron": "Iron Ore",
"Plank": "Wood",
"Fish": "Fish",
"Meat": "Carcass"
};
// Map input resource to output name for display
const REFINE_OUTPUT_MAP = {
"Gold": "Gold Bars",
"Iron": "Iron Bars",
"Plank": "Planks",
"Fish": "Sushi",
"Meat": "Meat"
};
// --- Helper Functions ---
function parseNumber(text) {
// Remove commas for thousands, parse float
return parseFloat(text.replace(/,/g, "")) || 0;
}
function secondsToHMS(seconds) {
let h = Math.floor(seconds / 3600);
let m = Math.floor((seconds % 3600) / 60);
let s = Math.floor(seconds % 60);
return `${h}h ${m}m ${s}s`;
}
// --- Skill Functions ---
function getActiveSkillName() {
const active = document.querySelector(".assign-btn.active");
if (!active) return null;
return active.innerText.split("\n")[0].trim();
}
function handleSkillSwitch() {
const currentSkill = getActiveSkillName();
if (currentSkill !== lastSkill) {
ignoreNextXP = true; // skip first XP log from previous skill
lastSkill = currentSkill;
}
}
function getActiveGatherResource() {
const skill = getActiveSkillName();
if (!skill) return null;
for (let key in GATHER_SKILL_MAP) {
if (skill.includes(key)) return GATHER_SKILL_MAP[key];
}
return null;
}
function getActiveRefine() {
const active = document.querySelector(".assign-btn.active");
if (!active) return null;
const info = active.querySelector(".skill-info");
if (!info) return null;
const text = info.innerText;
const inMatch = text.match(/In:\s*([\d,\.]+)/);
const outMatch = text.match(/Out:\s*([\d,\.]+)/);
if (!inMatch || !outMatch) return null;
const refineName = active.innerText.split("\n")[0];
let inputResource = null;
for (let key in REFINE_INPUT_MAP) {
if (refineName.includes(key)) inputResource = REFINE_INPUT_MAP[key];
}
const xpMatch = text.match(/earning\s*([\d,\.]+)\s*XP/i);
const xpPerAction = xpMatch ? parseNumber(xpMatch[1]) : 0;
return { name: refineName, ratioIn: parseNumber(inMatch[1]), ratioOut: parseNumber(outMatch[1]), inputResource, xpPerAction };
}
function getResourceAmount(resourceName) {
const grid = document.getElementById("pinned-resources-grid");
if (!grid) return 0;
const cards = grid.querySelectorAll(".resource-card");
for (let card of cards) {
if (card.title !== resourceName) continue;
let raw = card.querySelector(".res-value").innerText.trim();
// Remove commas only — correct thousands parsing
raw = raw.replace(/,/g, "");
return parseFloat(raw) || 0;
}
return 0;
}
// --- XP Log Observer ---
function observeXPLogs() {
const logContainer = document.getElementById("log-messages");
if (!logContainer) return setTimeout(observeXPLogs, 1000);
const observer = new MutationObserver(mutations => {
mutations.forEach(m => {
m.addedNodes.forEach(node => {
if (node.nodeType !== 1) return;
const text = node.innerText;
const xpMatch = text.match(/earning\s*([\d,\.]+)\s*\w+ XP/i);
if (xpMatch) {
const xpAmount = parseFloat(xpMatch[1].replace(/,/g, ""));
// --- Handle skill switch lag ---
if (ignoreNextXP) {
ignoreNextXP = false;
return;
}
// --- Instant XP/hour calculation ---
lastXPPerHour = xpAmount * (3600 / SECONDS_PER_ACTION);
}
});
});
});
observer.observe(logContainer, { childList: true, subtree: true });
}
// --- Gather/Refine Rates ---
function calculateGatherPerHour() {
handleSkillSwitch(); // check if skill changed
const refine = getActiveRefine();
// Refining
if (refine && refine.inputResource) {
const actionsPerHour = 3600 / SECONDS_PER_ACTION;
const outputPerHour = actionsPerHour * refine.ratioOut;
if (refine.xpPerAction) lastXPPerHour = refine.xpPerAction * actionsPerHour;
const outputName = REFINE_OUTPUT_MAP[refine.inputResource] || refine.name;
return { [outputName]: outputPerHour, xpPerHour: lastXPPerHour };
}
// Gathering
const grid = document.getElementById("pinned-resources-grid");
if (!grid) return null;
const cards = grid.querySelectorAll(".resource-card");
let currentSnapshot = {};
let newRates = {};
let detectedIncrease = false;
for (let card of cards) {
const name = card.title;
let raw = card.querySelector(".res-value").innerText.trim();
// Remove commas only — correct thousands parsing
raw = raw.replace(/,/g, "");
const value = parseFloat(raw) || 0;
currentSnapshot[name] = value;
if (lastResourceSnapshot[name] !== undefined) {
const diff = value - lastResourceSnapshot[name];
if (diff > 0) {
detectedIncrease = true;
newRates[name] = diff * (3600 / SECONDS_PER_ACTION);
}
}
}
lastResourceSnapshot = currentSnapshot;
if (detectedIncrease) {
lastGatherRates = newRates;
inactivityTicks = 0;
} else {
inactivityTicks++;
}
// Remove flicker wipe on inactivity, so no clearing lastGatherRates
// Always include instant XP/hour
lastGatherRates.xpPerHour = lastXPPerHour;
if (Object.keys(lastGatherRates).length === 0) return null;
return lastGatherRates;
}
// --- Update UI ---
function updateCalculator() {
let html = "";
const refine = getActiveRefine();
if (refine && refine.inputResource) {
const stock = getResourceAmount(refine.inputResource);
const actions = Math.floor(stock / refine.ratioIn);
const output = actions * refine.ratioOut;
const time = actions * SECONDS_PER_ACTION;
const outputName = REFINE_OUTPUT_MAP[refine.inputResource] || refine.name;
// === HEADER SHOWS REFINE <INPUT> ===
html += `<b>Refine ${refine.inputResource}</b><br>
${refine.inputResource} in stock: ${stock.toLocaleString('en-US')}<br>
In: ${refine.ratioIn.toLocaleString('en-US')} → Out: ${refine.ratioOut.toLocaleString('en-US')}
<hr>
Actions: ${actions.toLocaleString('en-US')}<br>
Total craft: ${output.toLocaleString('en-US')} ${outputName}<br>
Time: ${secondsToHMS(time)}
<hr>`;
}
const gather = calculateGatherPerHour();
if (gather) {
const label = (refine && refine.inputResource) ? "Refining Rate" : "Gather Rate";
html += `<b>${label}</b><br>`;
for (let resource in gather) {
if (resource !== "xpPerHour") {
html += `~${Math.round(gather[resource]).toLocaleString('en-US')} ${resource}/hour<br>`;
}
}
if (gather.xpPerHour) {
html += `<b>XP/hour:</b> ${Math.round(gather.xpPerHour).toLocaleString('en-US')} XP<br>`;
}
}
if (!html) html = "Waiting for activity…";
document.getElementById("calcOutput").innerHTML = html;
}
// --- Draggable Window ---
function makeDraggable(el) {
let isDown = false, offsetX = 0, offsetY = 0;
el.addEventListener("mousedown", e => {
isDown = true;
offsetX = el.offsetLeft - e.clientX;
offsetY = el.offsetTop - e.clientY;
});
document.addEventListener("mouseup", () => isDown = false);
document.addEventListener("mousemove", e => {
if (!isDown) return;
el.style.left = (e.clientX + offsetX) + "px";
el.style.top = (e.clientY + offsetY) + "px";
});
}
function createWindow() {
const box = document.createElement("div");
box.id = "refineCalcBox";
box.style.position = "fixed";
box.style.top = "200px";
box.style.left = "200px";
box.style.background = "#1e1e1e";
box.style.color = "white";
box.style.padding = "15px";
box.style.border = "2px solid #555";
box.style.zIndex = "999999";
box.style.width = "460px";
box.style.borderRadius = "8px";
box.innerHTML = `<b>Idle Artisan Tracker</b><hr><div id="calcOutput">Loading...</div>`;
document.body.appendChild(box);
makeDraggable(box);
}
// --- Init ---
window.addEventListener("load", function () {
createWindow();
observeXPLogs();
setInterval(updateCalculator, 1000);
});
})();