Idle Artisan Tracker Ultimate Mozilla Script

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

})();