Greasy Fork is available in English.

Mooneycalc-Importer

For the game MilkyWayIdle. This script imports player info to the following websites. https://mooneycalc.vercel.app/, https://mwisim.github.io/, https://cowculator.info/.

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==UserScript==
// @name         Mooneycalc-Importer
// @namespace    http://tampermonkey.net/
// @version      4.7
// @description  For the game MilkyWayIdle. This script imports player info to the following websites. https://mooneycalc.vercel.app/, https://mwisim.github.io/, https://cowculator.info/.
// @author       bot7420
// @match        https://www.milkywayidle.com/*
// @match        https://mooneycalc.vercel.app/*
// @match        https://kugandev.github.io/MWICombatSimulator/*
// @match        https://mwisim.github.io/*
// @match        https://cowculator.info/
// @match        http://43.129.194.214:5000/mwisim.github.io
// @match        http://127.0.0.1:5500/
// @run-at       document-start
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

/* 插件说明:http://43.129.194.214:5000/readme */

(function () {
    "use strict";

    if (document.URL.includes("milkywayidle.com")) {
        hookWS();
    } else if (document.URL.includes("mooneycalc.vercel.app")) {
        addImportButton1();
    } else if (document.URL.includes("kugandev.github.io/MWICombatSimulator")) {
        addImportButton2();
    } else if (document.URL.includes("mwisim.github.io") || document.URL.includes("127.0.0.1:5500")) {
        addImportButton3();
        observeResults();
    } else if (document.URL.includes("cowculator.info")) {
        addImportButton_cowculator();
    }

    function hookWS() {
        const dataProperty = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data");
        const oriGet = dataProperty.get;

        dataProperty.get = hookedGet;
        Object.defineProperty(MessageEvent.prototype, "data", dataProperty);

        function hookedGet() {
            const socket = this.currentTarget;
            if (!(socket instanceof WebSocket)) {
                return oriGet.call(this);
            }
            if (socket.url.indexOf("api.milkywayidle.com/ws") <= -1) {
                return oriGet.call(this);
            }

            const message = oriGet.call(this);
            Object.defineProperty(this, "data", { value: message }); // Anti-loop

            return handleMessage(message);
        }
    }

    function handleMessage(message) {
        let obj = JSON.parse(message);
        if (obj && obj.type === "init_character_data") {
            console.log(obj);
            GM_setValue("init_character_data", message);
        } else if (obj && obj.type === "init_client_data") {
            console.log(obj);
            GM_setValue("init_client_data", message);
        }
        return message;
    }

    function addImportButton1() {
        const checkElem = () => {
            const selectedElement = document.querySelector(`div[role="tablist"]`);
            if (selectedElement) {
                clearInterval(timer);
                console.log("Mooneycalc-Importer: Found elem");
                let button = document.createElement("button");
                selectedElement.parentNode.insertBefore(button, selectedElement.nextSibling);
                button.textContent = "导入人物数据 (刷新游戏网页更新人物数据; 左边Market设置里可以改进货价出货价)";
                button.style.backgroundColor = "green";
                button.style.padding = "5px";
                button.onclick = function () {
                    console.log("Mooneycalc-Importer: Button onclick");
                    importData1(button);
                    return false;
                };
            }
        };
        let timer = setInterval(checkElem, 200);
    }

    async function importData1(button) {
        let data = GM_getValue("init_character_data", "");
        let obj = JSON.parse(data);
        console.log(obj);
        if (!obj || !obj.characterSkills || !obj.currentTimestamp) {
            button.textContent = "错误:没有人物数据";
            return;
        }

        let ls = constructMooneycalcLocalStorage(obj);
        localStorage.setItem("settings", ls);

        let timestamp = new Date(obj.currentTimestamp).getTime();
        let now = new Date().getTime();
        button.textContent = "已导入,人物数据更新时间:" + timeReadable(now - timestamp) + " 前";

        await new Promise((r) => setTimeout(r, 500));
        location.reload();
    }

    function constructMooneycalcLocalStorage(obj) {
        const ls = localStorage.getItem("settings");
        let lsObj = JSON.parse(ls);

        // 人物技能等级
        lsObj.state.settings.levels = {};
        for (const skill of obj.characterSkills) {
            lsObj.state.settings.levels[skill.skillHrid] = skill.level;
        }

        // 社区全局buff
        lsObj.state.settings.communityBuffs = {};
        for (const buff of obj.communityBuffs) {
            lsObj.state.settings.communityBuffs[buff.hrid] = buff.level;
        }

        // 装备 & 装备强化等级
        lsObj.state.settings.equipment = {};
        lsObj.state.settings.equipmentLevels = {};
        for (const item of obj.characterItems) {
            if (item.itemLocationHrid !== "/item_locations/inventory") {
                lsObj.state.settings.equipment[item.itemLocationHrid.replace("item_locations", "equipment_types")] = item.itemHrid;
                lsObj.state.settings.equipmentLevels[item.itemLocationHrid.replace("item_locations", "equipment_types")] = item.enhancementLevel;
            }
        }

        // 房子
        lsObj.state.settings.houseRooms = {};
        for (const house of Object.values(obj.characterHouseRoomMap)) {
            lsObj.state.settings.houseRooms[house.houseRoomHrid] = house.level;
        }

        return JSON.stringify(lsObj);
    }

    function timeReadable(ms) {
        const d = new Date(1000 * Math.round(ms / 1000));
        function pad(i) {
            return ("0" + i).slice(-2);
        }
        let str = d.getUTCHours() + ":" + pad(d.getUTCMinutes()) + ":" + pad(d.getUTCSeconds());
        console.log("Mooneycalc-Importer: " + str);
        return str;
    }

    function addImportButton2() {
        const checkElem = () => {
            const selectedElement = document.querySelector(`button#buttonEquipmentSets`);
            if (selectedElement) {
                clearInterval(timer);
                console.log("Mooneycalc-Importer: Found elem");
                let button = document.createElement("button");
                selectedElement.parentNode.insertBefore(button, selectedElement.nextSibling);
                button.textContent = "导入人物数据 (刷新游戏网页更新人物数据; 不导入所有Trigers)";
                button.style.backgroundColor = "green";
                button.style.padding = "5px";
                button.onclick = function () {
                    console.log("Mooneycalc-Importer: Button onclick");
                    importData2(button);
                    return false;
                };
            }
        };
        let timer = setInterval(checkElem, 200);
    }

    async function importData2(button) {
        let data = GM_getValue("init_character_data", "");
        let obj = JSON.parse(data);
        console.log(obj);
        if (!obj || !obj.characterSkills || !obj.currentTimestamp) {
            button.textContent = "错误:没有人物数据";
            return;
        }

        fillIn2(obj);
        let timestamp = new Date(obj.currentTimestamp).getTime();
        let now = new Date().getTime();
        button.textContent = "已导入,人物数据更新时间:" + timeReadable(now - timestamp) + " 前";
    }

    function fillIn2(obj) {
        // Levels
        for (const skill of obj.characterSkills) {
            if (skill.skillHrid.includes("stamina")) {
                document.querySelector(`input#inputLevel_stamina`).value = skill.level;
            } else if (skill.skillHrid.includes("intelligence")) {
                document.querySelector(`input#inputLevel_intelligence`).value = skill.level;
            } else if (skill.skillHrid.includes("attack")) {
                document.querySelector(`input#inputLevel_attack`).value = skill.level;
            } else if (skill.skillHrid.includes("power")) {
                document.querySelector(`input#inputLevel_power`).value = skill.level;
            } else if (skill.skillHrid.includes("defense")) {
                document.querySelector(`input#inputLevel_defense`).value = skill.level;
            } else if (skill.skillHrid.includes("ranged")) {
                document.querySelector(`input#inputLevel_ranged`).value = skill.level;
            } else if (skill.skillHrid.includes("magic")) {
                document.querySelector(`input#inputLevel_magic`).value = skill.level;
            }
        }
        document.querySelector(`input#inputLevel_stamina`).dispatchEvent(new Event("change"));
        document.querySelector(`input#inputLevel_intelligence`).dispatchEvent(new Event("change"));
        document.querySelector(`input#inputLevel_attack`).dispatchEvent(new Event("change"));
        document.querySelector(`input#inputLevel_power`).dispatchEvent(new Event("change"));
        document.querySelector(`input#inputLevel_defense`).dispatchEvent(new Event("change"));
        document.querySelector(`input#inputLevel_ranged`).dispatchEvent(new Event("change"));
        document.querySelector(`input#inputLevel_magic`).dispatchEvent(new Event("change"));

        // Items
        for (const item of obj.characterItems) {
            if (item.itemLocationHrid.includes("/head")) {
                document.querySelector(`select#selectEquipment_head`).value = item.itemHrid;
                document.querySelector(`input#inputEquipmentEnhancementLevel_head`).value = item.enhancementLevel;
                document.querySelector(`select#selectEquipment_head`).dispatchEvent(new Event("change"));
                document.querySelector(`input#inputEquipmentEnhancementLevel_head`).dispatchEvent(new Event("change"));
            } else if (item.itemLocationHrid.includes("/body")) {
                document.querySelector(`select#selectEquipment_body`).value = item.itemHrid;
                document.querySelector(`input#inputEquipmentEnhancementLevel_body`).value = item.enhancementLevel;
                document.querySelector(`select#selectEquipment_body`).dispatchEvent(new Event("change"));
                document.querySelector(`input#inputEquipmentEnhancementLevel_body`).dispatchEvent(new Event("change"));
            } else if (item.itemLocationHrid.includes("/legs")) {
                document.querySelector(`select#selectEquipment_legs`).value = item.itemHrid;
                document.querySelector(`input#inputEquipmentEnhancementLevel_legs`).value = item.enhancementLevel;
                document.querySelector(`select#selectEquipment_legs`).dispatchEvent(new Event("change"));
                document.querySelector(`input#inputEquipmentEnhancementLevel_legs`).dispatchEvent(new Event("change"));
            } else if (item.itemLocationHrid.includes("/feet")) {
                document.querySelector(`select#selectEquipment_feet`).value = item.itemHrid;
                document.querySelector(`input#inputEquipmentEnhancementLevel_feet`).value = item.enhancementLevel;
                document.querySelector(`select#selectEquipment_feet`).dispatchEvent(new Event("change"));
                document.querySelector(`input#inputEquipmentEnhancementLevel_feet`).dispatchEvent(new Event("change"));
            } else if (item.itemLocationHrid.includes("/hands")) {
                document.querySelector(`select#selectEquipment_hands`).value = item.itemHrid;
                document.querySelector(`input#inputEquipmentEnhancementLevel_hands`).value = item.enhancementLevel;
                document.querySelector(`select#selectEquipment_hands`).dispatchEvent(new Event("change"));
                document.querySelector(`input#inputEquipmentEnhancementLevel_hands`).dispatchEvent(new Event("change"));
            } else if (item.itemLocationHrid.includes("/main_hand")) {
                document.querySelector(`select#selectEquipment_weapon`).value = item.itemHrid;
                document.querySelector(`input#inputEquipmentEnhancementLevel_weapon`).value = item.enhancementLevel;
                document.querySelector(`select#selectEquipment_weapon`).dispatchEvent(new Event("change"));
                document.querySelector(`input#inputEquipmentEnhancementLevel_weapon`).dispatchEvent(new Event("change"));
            } else if (item.itemLocationHrid.includes("/off_hand")) {
                document.querySelector(`select#selectEquipment_off_hand`).value = item.itemHrid;
                document.querySelector(`input#inputEquipmentEnhancementLevel_off_hand`).value = item.enhancementLevel;
                document.querySelector(`select#selectEquipment_off_hand`).dispatchEvent(new Event("change"));
                document.querySelector(`input#inputEquipmentEnhancementLevel_off_hand`).dispatchEvent(new Event("change"));
            } else if (item.itemLocationHrid.includes("/pouch")) {
                document.querySelector(`select#selectEquipment_pouch`).value = item.itemHrid;
                document.querySelector(`input#inputEquipmentEnhancementLevel_pouch`).value = item.enhancementLevel;
                document.querySelector(`select#selectEquipment_pouch`).dispatchEvent(new Event("change"));
                document.querySelector(`input#inputEquipmentEnhancementLevel_pouch`).dispatchEvent(new Event("change"));
            }
        }

        // Food
        let foodIndex = 0;
        for (const food of obj.actionTypeFoodSlotsMap["/action_types/combat"]) {
            if (food) {
                document.querySelector(`select#selectFood_${foodIndex}`).value = food.itemHrid;
                document.querySelector(`select#selectFood_${foodIndex++}`).dispatchEvent(new Event("change"));
            }
        }

        // Drinks
        let drinksIndex = 0;
        for (const drink of obj.actionTypeDrinkSlotsMap["/action_types/combat"]) {
            if (drink) {
                document.querySelector(`select#selectDrink_${drinksIndex}`).value = drink.itemHrid;
                document.querySelector(`select#selectDrink_${drinksIndex++}`).dispatchEvent(new Event("change"));
            }
        }

        // Abilities
        let abilityIndex = 0;
        for (const ability of obj.combatUnit.combatAbilities) {
            if (ability) {
                document.querySelector(`select#selectAbility_${abilityIndex}`).value = ability.abilityHrid;
                document.querySelector(`input#inputAbilityLevel_${abilityIndex}`).value = ability.level;
                document.querySelector(`select#selectAbility_${abilityIndex}`).dispatchEvent(new Event("change"));
                document.querySelector(`input#inputAbilityLevel_${abilityIndex++}`).dispatchEvent(new Event("change"));
            }
        }

        // Zone
        for (const action of obj.characterActions) {
            if (action && action.actionHrid.includes("/actions/combat/")) {
                document.querySelector(`select#selectZone`).value = action.actionHrid;
                document.querySelector(`select#selectZone`).dispatchEvent(new Event("change"));
                break;
            }
        }

        // todo Trigers consumableCombatTriggersMap abilityCombatTriggersMap
    }

    function addImportButton3() {
        const checkElem = () => {
            const selectedElement = document.querySelector(`button#buttonImportExport`);
            if (selectedElement) {
                clearInterval(timer);
                console.log("Mooneycalc-Importer: Found elem");
                let button = document.createElement("button");
                selectedElement.parentNode.parentElement.parentElement.insertBefore(button, selectedElement.parentElement.parentElement.nextSibling);
                button.textContent = "导入人物数据(刷新游戏网页更新人物数据)";
                button.style.backgroundColor = "green";
                button.style.padding = "5px";
                button.onclick = function () {
                    console.log("Mooneycalc-Importer: Button onclick");
                    const getPriceButton = document.querySelector(`button#buttonGetPrices`);
                    if (getPriceButton) {
                        console.log("Click getPriceButton");
                        getPriceButton.click();
                    }
                    importData3(button);
                    return false;
                };
            }
        };
        let timer = setInterval(checkElem, 200);
    }

    async function importData3(button) {
        let data = GM_getValue("init_character_data", "");
        let obj = JSON.parse(data);
        console.log(obj);
        if (!obj || !obj.characterSkills || !obj.currentTimestamp) {
            button.textContent = "错误:没有人物数据";
            return;
        }

        let jsonObj = constructImportJsonObj(obj);
        console.log(jsonObj);
        const importInputElem = document.querySelector(`input#inputSet`);
        importInputElem.value = JSON.stringify(jsonObj);
        document.querySelector(`button#buttonImportSet`).click();

        let timestamp = new Date(obj.currentTimestamp).getTime();
        let now = new Date().getTime();
        button.textContent = "导入成功。人物数据更新时间:" + timeReadable(now - timestamp) + " 前";

        setTimeout(() => {
            document.querySelector(`button#buttonStartSimulation`).click();
        }, 500);
    }

    function constructImportJsonObj(obj) {
        let clientObj = JSON.parse(GM_getValue("init_client_data", ""));

        let exportObj = {};

        exportObj.player = {};
        // Levels
        for (const skill of obj.characterSkills) {
            if (skill.skillHrid.includes("stamina")) {
                exportObj.player.staminaLevel = skill.level;
            } else if (skill.skillHrid.includes("intelligence")) {
                exportObj.player.intelligenceLevel = skill.level;
            } else if (skill.skillHrid.includes("attack")) {
                exportObj.player.attackLevel = skill.level;
            } else if (skill.skillHrid.includes("power")) {
                exportObj.player.powerLevel = skill.level;
            } else if (skill.skillHrid.includes("defense")) {
                exportObj.player.defenseLevel = skill.level;
            } else if (skill.skillHrid.includes("ranged")) {
                exportObj.player.rangedLevel = skill.level;
            } else if (skill.skillHrid.includes("magic")) {
                exportObj.player.magicLevel = skill.level;
            }
        }
        // Items
        exportObj.player.equipment = [];
        for (const item of obj.characterItems) {
            if (!item.itemLocationHrid.includes("/item_locations/inventory")) {
                exportObj.player.equipment.push({
                    itemLocationHrid: item.itemLocationHrid,
                    itemHrid: item.itemHrid,
                    enhancementLevel: item.enhancementLevel,
                });
            }
        }

        // Food
        exportObj.food = {};
        exportObj.food["/action_types/combat"] = [];
        for (const food of obj.actionTypeFoodSlotsMap["/action_types/combat"]) {
            if (food) {
                exportObj.food["/action_types/combat"].push({
                    itemHrid: food.itemHrid,
                });
            } else {
                exportObj.food["/action_types/combat"].push({
                    itemHrid: "",
                });
            }
        }

        // Drinks
        exportObj.drinks = {};
        exportObj.drinks["/action_types/combat"] = [];
        for (const drink of obj.actionTypeDrinkSlotsMap["/action_types/combat"]) {
            if (drink) {
                exportObj.drinks["/action_types/combat"].push({
                    itemHrid: drink.itemHrid,
                });
            } else {
                exportObj.drinks["/action_types/combat"].push({
                    itemHrid: "",
                });
            }
        }

        // Abilities
        exportObj.abilities = [
            {
                abilityHrid: "",
                level: "1",
            },
            {
                abilityHrid: "",
                level: "1",
            },
            {
                abilityHrid: "",
                level: "1",
            },
            {
                abilityHrid: "",
                level: "1",
            },
            {
                abilityHrid: "",
                level: "1",
            },
        ];
        let normalAbillityIndex = 1;
        for (const ability of obj.combatUnit.combatAbilities) {
            if (ability && clientObj.abilityDetailMap[ability.abilityHrid].isSpecialAbility) {
                exportObj.abilities[0] = {
                    abilityHrid: ability.abilityHrid,
                    level: ability.level,
                };
            } else if (ability) {
                exportObj.abilities[normalAbillityIndex++] = {
                    abilityHrid: ability.abilityHrid,
                    level: ability.level,
                };
            }
        }

        // TriggerMap
        exportObj.triggerMap = { ...obj.abilityCombatTriggersMap, ...obj.consumableCombatTriggersMap };

        // Zone
        let hasMap = false;
        for (const action of obj.characterActions) {
            if (action && action.actionHrid.includes("/actions/combat/")) {
                hasMap = true;
                exportObj.zone = action.actionHrid;
                break;
            }
        }
        if (!hasMap) {
            exportObj.zone = "/actions/combat/fly";
        }

        // SimulationTime
        exportObj.simulationTime = "24";

        // HouseRooms
        exportObj.houseRooms = {};
        for (const house of Object.values(obj.characterHouseRoomMap)) {
            exportObj.houseRooms[house.houseRoomHrid] = house.level;
        }

        return exportObj;
    }

    async function observeResults() {
        let resultDiv = document.querySelector(`div.row`)?.querySelectorAll(`div.col-md-5`)?.[2]?.querySelector(`div.row > div.col-md-5`);
        while (!resultDiv) {
            await new Promise((resolve) => setTimeout(resolve, 100));
            resultDiv = document.querySelector(`div.row`)?.querySelectorAll(`div.col-md-5`)?.[2]?.querySelector(`div.row > div.col-md-5`);
        }

        const deathDiv = document.querySelector(`div#simulationResultPlayerDeaths`);
        const expDiv = document.querySelector(`div#simulationResultExperienceGain`);
        const consumeDiv = document.querySelector(`div#simulationResultConsumablesUsed`);
        deathDiv.style.backgroundColor = "#FFEAE9";
        deathDiv.style.color = "black";
        expDiv.style.backgroundColor = "#CDFFDD";
        expDiv.style.color = "black";
        consumeDiv.style.backgroundColor = "#F0F8FF";
        consumeDiv.style.color = "black";

        let div = document.createElement("div");
        div.id = "tillLevel";
        div.style.backgroundColor = "#FFFFE0";
        div.style.color = "black";
        div.textContent = "";
        resultDiv.append(div);

        new MutationObserver((mutationsList) => {
            mutationsList.forEach((mutation) => {
                if (mutation.addedNodes.length >= 3) {
                    handleResult(mutation.addedNodes, div);
                }
            });
        }).observe(expDiv, { childList: true, subtree: true });
    }

    function handleResult(expNodes, parentDiv) {
        let perHourGainExp = {
            stamina: 0,
            intelligence: 0,
            attack: 0,
            power: 0,
            defense: 0,
            ranged: 0,
            magic: 0,
        };

        expNodes.forEach((expNodes) => {
            if (expNodes.textContent.includes("Stamina")) {
                perHourGainExp.stamina = Number(expNodes.children[1].textContent);
            } else if (expNodes.textContent.includes("Intelligence")) {
                perHourGainExp.intelligence = Number(expNodes.children[1].textContent);
            } else if (expNodes.textContent.includes("Attack")) {
                perHourGainExp.attack = Number(expNodes.children[1].textContent);
            } else if (expNodes.textContent.includes("Power")) {
                perHourGainExp.power = Number(expNodes.children[1].textContent);
            } else if (expNodes.textContent.includes("Defense")) {
                perHourGainExp.defense = Number(expNodes.children[1].textContent);
            } else if (expNodes.textContent.includes("Ranged")) {
                perHourGainExp.ranged = Number(expNodes.children[1].textContent);
            } else if (expNodes.textContent.includes("Magic")) {
                perHourGainExp.magic = Number(expNodes.children[1].textContent);
            }
        });

        let data = GM_getValue("init_character_data", null);
        let obj = JSON.parse(data);
        if (!obj || !obj.characterSkills || !obj.currentTimestamp) {
            console.error("handleResult no character localstorage");
            return;
        }

        let skillLevels = {};
        for (const skill of obj.characterSkills) {
            if (skill.skillHrid.includes("stamina")) {
                skillLevels.stamina = {};
                skillLevels.stamina.skillName = "Stamina";
                skillLevels.stamina.currentLevel = skill.level;
                skillLevels.stamina.currentExp = skill.experience;
            } else if (skill.skillHrid.includes("intelligence")) {
                skillLevels.intelligence = {};
                skillLevels.intelligence.skillName = "Intelligence";
                skillLevels.intelligence.currentLevel = skill.level;
                skillLevels.intelligence.currentExp = skill.experience;
            } else if (skill.skillHrid.includes("attack")) {
                skillLevels.attack = {};
                skillLevels.attack.skillName = "Attack";
                skillLevels.attack.currentLevel = skill.level;
                skillLevels.attack.currentExp = skill.experience;
            } else if (skill.skillHrid.includes("power")) {
                skillLevels.power = {};
                skillLevels.power.skillName = "Power";
                skillLevels.power.currentLevel = skill.level;
                skillLevels.power.currentExp = skill.experience;
            } else if (skill.skillHrid.includes("defense")) {
                skillLevels.defense = {};
                skillLevels.defense.skillName = "Defense";
                skillLevels.defense.currentLevel = skill.level;
                skillLevels.defense.currentExp = skill.experience;
            } else if (skill.skillHrid.includes("ranged")) {
                skillLevels.ranged = {};
                skillLevels.ranged.skillName = "Ranged";
                skillLevels.ranged.currentLevel = skill.level;
                skillLevels.ranged.currentExp = skill.experience;
            } else if (skill.skillHrid.includes("magic")) {
                skillLevels.magic = {};
                skillLevels.magic.skillName = "Magic";
                skillLevels.magic.currentLevel = skill.level;
                skillLevels.magic.currentExp = skill.experience;
            }
        }

        const skillNamesInOrder = ["stamina", "intelligence", "attack", "power", "defense", "ranged", "magic"];
        let hTMLStr = "";
        for (const skill of skillNamesInOrder) {
            hTMLStr += `<div id="${"inputDiv_" + skill}" style="display: flex; justify-content: flex-end">${skillLevels[skill].skillName}到<input id="${"input_" + skill}" type="number" value="${
                skillLevels[skill].currentLevel + 1
            }" min="${skillLevels[skill].currentLevel + 1}" max="200">级</div>`;
        }
        hTMLStr += `<div id="needDiv"></div>`;
        hTMLStr += `<div id="needListDiv"></div>`;
        parentDiv.innerHTML = hTMLStr;

        for (const skill of skillNamesInOrder) {
            const skillDiv = parentDiv.querySelector(`div#${"inputDiv_" + skill}`);
            const skillInput = parentDiv.querySelector(`input#${"input_" + skill}`);
            skillInput.onchange = () => {
                calculateTill(skill, skillInput, skillLevels, parentDiv, perHourGainExp);
            };
            skillInput.addEventListener("keyup", function (evt) {
                calculateTill(skill, skillInput, skillLevels, parentDiv, perHourGainExp);
            });
            skillDiv.onclick = () => {
                calculateTill(skill, skillInput, skillLevels, parentDiv, perHourGainExp);
            };
        }

        // 提取成本和收益
        const expensesSpan = document.querySelector(`span#expensesSpan`);
        const revenueSpan = document.querySelector(`span#revenueSpan`);
        const profitSpan = document.querySelector(`span#profitPreview`);
        const expenseDiv = document.querySelector(`div#script_expense`);
        const revenueDiv = document.querySelector(`div#script_revenue`);
        if (expenseDiv && expenseDiv) {
            expenseDiv.textContent = expensesSpan.parentNode.textContent;
            revenueDiv.textContent = revenueSpan.parentNode.textContent;
        } else {
            profitSpan.parentNode.insertAdjacentHTML(
                "beforeend",
                `<div id="script_expense" style="background-color: #DCDCDC; color: black;">${expensesSpan.parentNode.textContent}</div><div id="script_revenue" style="background-color: #DCDCDC; color: black;">${revenueSpan.parentNode.textContent}</div>`
            );
        }
    }

    function calculateTill(skillName, skillInputElem, skillLevels, parentDiv, perHourGainExp) {
        const initData_levelExperienceTable = JSON.parse(GM_getValue("init_client_data", null)).levelExperienceTable;
        const targetLevel = Number(skillInputElem.value);
        parentDiv.querySelector(`div#needDiv`).textContent = `${skillLevels[skillName].skillName} 到 ${targetLevel} 级 还需:`;
        const listDiv = parentDiv.querySelector(`div#needListDiv`);

        const currentLevel = Number(skillLevels[skillName].currentLevel);
        const currentExp = Number(skillLevels[skillName].currentExp);
        if (targetLevel > currentLevel && targetLevel <= 200) {
            if (perHourGainExp[skillName] === 0) {
                listDiv.innerHTML = "永远";
            } else {
                let needExp = initData_levelExperienceTable[targetLevel] - currentExp;
                let needHours = needExp / perHourGainExp[skillName];
                let html = "";
                html += `<div>[${hoursToReadableString(needHours)}]</div>`;

                const consumeDivs = document.querySelectorAll(`div#simulationResultConsumablesUsed div.row`);
                for (const elem of consumeDivs) {
                    const conName = elem.children[0].textContent;
                    const conPerHour = Number(elem.children[1].textContent);
                    html += `<div>${conName} ${Number(conPerHour * needHours).toFixed(0)}</div>`;
                }

                listDiv.innerHTML = html;
            }
        } else {
            listDiv.innerHTML = "输入错误";
        }
    }

    function hoursToReadableString(hours) {
        const sec = hours * 60 * 60;
        if (sec >= 86400) {
            return Number(sec / 86400).toFixed(1) + " 天";
        }
        const d = new Date(Math.round(sec * 1000));
        function pad(i) {
            return ("0" + i).slice(-2);
        }
        let str = d.getUTCHours() + "h " + pad(d.getUTCMinutes()) + "m " + pad(d.getUTCSeconds()) + "s";
        return str;
    }

    /* For cowculator.info */
    function addImportButton_cowculator() {
        const checkElem = () => {
            const selectedElement = document.querySelector(`ul.menu.sticky.text-base-content`);
            if (selectedElement) {
                clearInterval(timer);
                console.log("Mooneycalc-Importer: Found elem");
                let button = document.createElement("button");
                selectedElement.parentNode.append(button);
                button.textContent = "导入人物数据(导入会删除已保存的套装配置;刷新游戏网页更新人物数据)";
                button.style.backgroundColor = "green";
                button.style.padding = "5px";
                button.style.width = "200px";
                button.onclick = function () {
                    console.log("Mooneycalc-Importer: Button onclick");
                    importData_cowculator(button);
                    return false;
                };
            }
        };
        let timer = setInterval(checkElem, 200);
    }

    async function importData_cowculator(button) {
        let data = GM_getValue("init_character_data", "");
        let obj = JSON.parse(data);
        console.log(obj);
        if (!obj || !obj.characterSkills || !obj.currentTimestamp) {
            button.textContent = "错误:没有人物数据";
            return;
        }

        let isSuccess = constructImportJsonObj_cowculator(obj);
        if (isSuccess) {
            let timestamp = new Date(obj.currentTimestamp).getTime();
            let now = new Date().getTime();
            button.textContent = "导入成功。人物数据更新时间:" + timeReadable(now - timestamp) + " 前";
            await new Promise((r) => setTimeout(r, 500));
            location.reload();
        } else {
            button.textContent = "导入失败,请查看浏览器控制台报错";
        }
    }

    function constructImportJsonObj_cowculator(obj) {
        let data = GM_getValue("init_client_data", "");
        let clientObj = JSON.parse(data);

        let house = {};
        for (const h of Object.values(obj.characterHouseRoomMap)) {
            house[h.houseRoomHrid] = h.level;
        }
        console.log(house);
        localStorage.setItem("house", JSON.stringify(house));

        let characterLevels = {};
        let targetLevels = {};
        let currentXp = {};
        for (const skill of obj.characterSkills) {
            characterLevels[skill.skillHrid] = skill.level;
            targetLevels[skill.skillHrid] = skill.level + 1;
            currentXp[skill.skillHrid] = skill.experience;
        }
        console.log(characterLevels);
        localStorage.setItem("characterLevels", JSON.stringify(characterLevels));
        console.log(targetLevels);
        localStorage.setItem("targetLevels", JSON.stringify(targetLevels));
        console.log(currentXp);
        localStorage.setItem("currentXp", JSON.stringify(currentXp));

        let loadout = {
            activeLoadoutId: 0,
            loadouts: {
                0: {
                    id: 0,
                    name: "default",
                    equipment: {
                        "/item_locations/head": null,
                        "/item_locations/body": null,
                        "/item_locations/legs": null,
                        "/item_locations/feet": null,
                        "/item_locations/hands": null,
                        "/item_locations/main_hand": null,
                        "/item_locations/off_hand": null,
                        "/item_locations/earrings": null,
                        "/item_locations/neck": null,
                        "/item_locations/ring": null,
                        "/item_locations/pouch": null,
                        "/item_locations/milking_tool": null,
                        "/item_locations/foraging_tool": null,
                        "/item_locations/woodcutting_tool": null,
                        "/item_locations/cheesesmithing_tool": null,
                        "/item_locations/crafting_tool": null,
                        "/item_locations/tailoring_tool": null,
                        "/item_locations/cooking_tool": null,
                        "/item_locations/brewing_tool": null,
                        "/item_locations/enhancing_tool": null,
                    },
                    enhancementLevels: {
                        "/item_locations/head": 0,
                        "/item_locations/body": 0,
                        "/item_locations/legs": 0,
                        "/item_locations/feet": 0,
                        "/item_locations/hands": 0,
                        "/item_locations/main_hand": 0,
                        "/item_locations/off_hand": 0,
                        "/item_locations/earrings": 0,
                        "/item_locations/neck": 0,
                        "/item_locations/ring": 0,
                        "/item_locations/pouch": 0,
                        "/item_locations/milking_tool": 0,
                        "/item_locations/foraging_tool": 0,
                        "/item_locations/woodcutting_tool": 0,
                        "/item_locations/cheesesmithing_tool": 0,
                        "/item_locations/crafting_tool": 0,
                        "/item_locations/tailoring_tool": 0,
                        "/item_locations/cooking_tool": 0,
                        "/item_locations/brewing_tool": 0,
                        "/item_locations/enhancing_tool": 0,
                    },
                },
            },
        };
        for (const item of obj.characterItems) {
            if (item.itemLocationHrid !== "/item_locations/inventory") {
                loadout.loadouts[0].equipment[item.itemLocationHrid] = clientObj.itemDetailMap[item.itemHrid];
                loadout.loadouts[0].enhancementLevels[item.itemLocationHrid] = item.enhancementLevel;
            }
        }
        console.log(loadout);
        localStorage.setItem("loadout", JSON.stringify(loadout));

        let skillDrinks = {
            "/action_types/milking": [],
            "/action_types/foraging": [],
            "/action_types/woodcutting": [],
            "/action_types/cheesesmithing": [],
            "/action_types/crafting": [],
            "/action_types/tailoring": [],
            "/action_types/cooking": [],
            "/action_types/brewing": [],
            "/action_types/enhancing": [],
            "/action_types/combat": [],
        };
        for (const key of Object.keys(obj.actionTypeDrinkSlotsMap)) {
            if (key.includes("combat")) {
                continue;
            }
            for (const drink of obj.actionTypeDrinkSlotsMap[key]) {
                if (!drink) {
                    continue;
                }
                const itemDetail = clientObj.itemDetailMap[drink.itemHrid];
                skillDrinks[key].push(itemDetail);
            }
        }
        console.log(skillDrinks);
        localStorage.setItem("skillDrinks", JSON.stringify(skillDrinks));

        return true;
    }
})();