Idle Artisan Tracker Ultimate Mozilla Script

Real-time gathering, refining, and XP/hour calculator for Idle Artisan.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==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);
});

})();