Idle Artisan Tracker Ultimate Mozilla Script

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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

})();