Gear set manager

Hordes.io script

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Gear set manager
// @version      1.2
// @description  Hordes.io script
// @match        https://hordes.io/play
// @run-at       document-start
// @grant        none
// @namespace https://greasyfork.org/users/1583443
// ==/UserScript==

(function() {
    const config = {
        equipDelay: 250,
        stashDelay: 250,
        unstashDelay: 500,
        stashCharms: false,
        retryDelay: 1000,
        maxRetryAmount: 1,
    };

    const itemEquipSlots = {
        hammer: [101],
        bow: [101],
        staff: [101],
        sword: [101],
        armlet: [102],
        armor: [103],
        bag: [104],
        boot: [105],
        glove: [106],
        ring: [107],
        amulet: [108],
        quiver: [109],
        shield: [109],
        totem: [109],
        orb: [109],
        charm: [110, 111],
    };
    const equipSlotMin = 101;

    let inventorySlots = new Map();
    let loadedItems = new Map();
    let stashSlots = new Set();
    let currentPlayerName = null;
    let selectedGearSet = null;
    let selectedStashSet = null;
    let gearSetNameInput = "";

    let ws = null;

    let z = 0;
    var ye = t => {
        let e = 0,
            n = 0,
            o = 0;
        do o = t[z++], e |= (o & 127) << 7 * n, n++; while (o & 128);
        return e
    };
    var tr = (t, e) => {
            for (; e > 127;) t[z++] = e & 127 | 128, e >>= 7;
            t[z++] = e & 127
        },
        ti = t => t <= 0 ? 1 : Math.floor(Math.log(t) / Math.log(128)) + 1,
        bi = t => t[z] & 128 ? (255 - t[z] + 1) * -1 : t[z],
        Le = t => t[z],
        Ki = (t, e) => {
            t[z] = e, z += 1
        },
        Ot = t => t[z] | t[z + 1] << 8;
    var po = t => (t[z] | t[z + 1] << 8 | t[z + 2] << 16) + t[z + 3] * 16777216,
        p1 = new ArrayBuffer(8),
        nr = new Uint8Array(p1),
        _3 = new Float32Array(p1),
        lt = t => (nr[0] = t[z], nr[1] = t[z + 1], nr[2] = t[z + 2], nr[3] = t[z + 3], _3[0]);
    var y3 = t => {
            let e = t.length;
            for (let n = t.length - 1; n >= 0; n--) {
                let o = t.charCodeAt(n);
                o > 127 && o <= 2047 ? e++ : o > 2047 && o <= 65535 && (e += 2), o >= 56320 && o <= 57343 && n--
            }
            return e
        },
        v3 = (t, e) => {
            let n = y3(e);
            tr(t, n);
            for (let o = 0; o < e.length; o++) {
                let i = e.charCodeAt(o);
                i < 128 ? t[z++] = i : i < 2048 ? (t[z++] = i >> 6 | 192, t[z++] = i & 63 | 128) : (i & 64512) == 55296 && o + 1 < e.length && (e.charCodeAt(o + 1) & 64512) == 56320 ? (i = 65536 + ((i & 1023) << 10) + (e.charCodeAt(++o) & 1023), t[z++] = i >> 18 | 240, t[z++] = i >> 12 & 63 | 128, t[z++] = i >> 6 & 63 | 128, t[z++] = i & 63 | 128) : (t[z++] = i >> 12 | 224, t[z++] = i >> 6 & 63 | 128, t[z++] = i & 63 | 128)
            }
        };
    var h3 = {
        string: t => {
            let e = y3(t);
            return ti(e) + e
        }
    };
    var C3 = {
            encode: t => {
                let e = t,
                    n = 0;
                n += 1, n += h3.string(e.command), n += h3.string(e.string);
                let o = new Uint8Array(n);
                return z = 0, Ki(o, e._header), v3(o, e.command), v3(o, e.string), o
            }
        },
        S3 = {
            decode: t => {
                let e = t,
                    n = {};
                z = 0, n._header = Le(e), z += 1;
                let o = [];
                n.inputs = o;
                let i = ye(e);
                for (let u = 0; u < i; u++) {
                    let m = {};
                    o[u] = m, m.id = po(e), z += 4, m.jump = Le(e), z += 1, m.rot = lt(e), z += 4, m.speed = Ot(e), z += 2;
                    let g = [];
                    m.steer = g, g[0] = bi(e), z += 1;
                    let v = ye(e);
                    for (let _ = 1; _ < v; _++) g[_] = bi(e), z += 1
                }
                let s = [];
                n.log = s;
                let r = ye(e);
                for (let u = 0; u < r; u++) {
                    let m = {};
                    s[u] = m;
                    let g = [];
                    m.data = g;
                    let v = ye(e);
                    for (let _ = 0; _ < v; _++) g[_] = ye(e);
                    m.type = Le(e), z += 1
                }
                let l = [];
                n.logPersonal = l;
                let a = ye(e);
                for (let u = 0; u < a; u++) {
                    let m = {};
                    l[u] = m;
                    let g = [];
                    m.data = g;
                    let v = ye(e);
                    for (let _ = 0; _ < v; _++) g[_] = ye(e);
                    m.type = Le(e), z += 1
                }
                let c = [];
                n.movements = c;
                let f = ye(e);
                for (let u = 0; u < f; u++) {
                    let m = {};
                    c[u] = m, m.id = po(e), z += 4;
                    let g = [];
                    m.pos = g, g[0] = lt(e), z += 4, g[1] = lt(e), z += 4;
                    let v = ye(e);
                    for (let k = 2; k < v; k++) g[k] = lt(e), z += 4;
                    let _ = [];
                    m.vel = _, _[0] = lt(e), z += 4, _[1] = lt(e), z += 4;
                    let b = ye(e);
                    for (let k = 2; k < b; k++) _[k] = lt(e), z += 4
                }
                return n.tickId = po(e), z += 4, n
            }
        };

    var send = (t, e = "") => {
        if (!ws || ws.readyState !== 1) return;
        ws.send(C3.encode({
            _header: 5,
            command: t,
            string: e + ""
        }))
    };

    const moveItem = (fromSlot, toSlot) => send("itemmove", `${fromSlot} ${toSlot}`);
    const clientPlayerCommand = (cmd, data) => send(cmd, data);

    function handlePersonalLog(events) {
        let needsRefresh = false;
        for (let {
                type,
                data
            }
            of events) {
            if (type === 29) {
                inventorySlots.set(data[0], data[1]);
                needsRefresh = true;
                continue;
            }
            if (type === 30) {
                inventorySlots.delete(data[0]);
                needsRefresh = true;
                continue;
            }
            if (type === 38) {
                stashSlots.clear();
                let i = 2;
                for (let g = 0; g < 2; g++) {
                    let count = data[i++];
                    for (let j = 0; j < count; j++) stashSlots.add(data[i++]);
                }
                needsRefresh = true;
            }
        }
        if (needsRefresh) refreshUI();
    }

    function interceptSocket(socket) {
        ws = socket;

        socket.addEventListener("message", (event) => {
            if (!(event.data instanceof ArrayBuffer)) return;
            let data = new Uint8Array(event.data);
            if (data[0] !== 7) return;
            try {
                let decoded = S3.decode(data);
                if (decoded.logPersonal && decoded.logPersonal.length) handlePersonalLog(decoded.logPersonal);
            } catch (err) {
                console.log(err)
            }
        });

        socket.addEventListener("close", () => {
            if (ws === socket) ws = null;
        });
    }

    function wsProxy(target, params) {
        let ws = new target(...params)
        if (typeof params[0] === "string" && params[0].includes("hordes.io")) {
            interceptSocket(ws);
        }
        return ws;
    }

    let Ws = window.WebSocket;
    window.WebSocket = new Proxy(Ws, {
        construct: wsProxy
    });

    function fetchProxy(target, _, params) {
        let result = target.apply(window, params);
        let urlMatch = typeof params[0] === "string" && params[0].includes("/api/item/get");
        if (!urlMatch) return result;
        result.then(async r => {
            let items = await r.clone().json();
            if (!Array.isArray(items)) return;

            for (let item of items) {
                if (!item || item.slot == null) continue;
                let dbid = item.dbid ?? inventorySlots.get(item.slot);
                if (dbid == null || item.type == null) continue;
                loadedItems.set(dbid, item);
            }

            refreshUI();
        }).catch((err) => {
            console.log(err)
        });

        return result;
    }

    let origFetch = window.fetch;
    window.fetch = new Proxy(origFetch, {
        apply: fetchProxy
    });

    const delay = ms => new Promise(r => setTimeout(r, ms));

    function localStorageRead(key) {
        return JSON.parse(localStorage.getItem("savedGearSets"));
    }

    function localStorageWrite(key, value) {
        return localStorage.setItem(key, JSON.stringify(value));
    }

    function readAllSets() {
        try {
            return localStorageRead("savedGearSets") || [];
        } catch (err) {
            return [];
        }
    }

    function writeAllSets(sets) {
        localStorageWrite("savedGearSets", sets);
    }

    function getSetByValue(value) {
        return readAllSets().find(s => s.value === value);
    }

    function getCurrentPlayerName() {
        if (currentPlayerName) return currentPlayerName;

        let equipSlots = document.getElementById("equipslots");
        if (equipSlots) {
            let charPanel = equipSlots.parentElement;
            if (charPanel) {
                let nameSpan = charPanel.querySelector(".statcol.panel-black span.bold.textwhite");
                if (nameSpan && nameSpan.textContent) {
                    currentPlayerName = nameSpan.textContent;
                    return currentPlayerName;
                }
            }
        }

        let sets = readAllSets();
        if (sets.length) return sets[sets.length - 1].playerId;
        return null;
    }

    function getPlayerSets() {
        let name = getCurrentPlayerName();
        if (!name) return [];
        return readAllSets().filter(s => s.playerId === name);
    }

    function findFirstEmptyBagSlot(exclude = new Set()) {
        for (let i = 0; i < 100; i++) {
            if (!inventorySlots.has(i) && !exclude.has(i)) return i;
        }
        return -1;
    }

    function gearSetCreate() {
        let equipped = [];
        let pid = getCurrentPlayerName();

        inventorySlots.forEach((dbid, slot) => {
            if (slot >= equipSlotMin) equipped.push(dbid);
        });
        if (!equipped.length || !pid) return;

        let name = gearSetNameInput || "unnamed"
        let sets = readAllSets();
        let equippedSet = new Set(equipped);

        let isDuplicate = sets.some(set => {
            if (set.playerId !== pid) return false;
            let items = set.items || [];
            return items.length === equipped.length && items.every(id => equippedSet.has(id));
        });
        if (isDuplicate) return;

        let value = Date.now().toString();
        sets.push({
            name: name,
            value,
            items: equipped,
            playerId: pid
        });
        writeAllSets(sets);

        selectedGearSet = value;
        gearSetNameInput = "";
        refreshUI();
    }

    function gearSetDelete() {
        if (!selectedGearSet) return;
        let filtered = readAllSets().filter(s => s.value !== selectedGearSet);
        writeAllSets(filtered);

        let remaining = getPlayerSets().filter(s => s.value !== selectedGearSet);
        selectedGearSet = remaining.length ? remaining[0].value : null;
        refreshUI();
    }

    async function gearSetEquip(retry = 0) {
        let gearSet = selectedGearSet ? getSetByValue(selectedGearSet) : null;
        if (!gearSet) return;

        let targetIds = new Set(gearSet.items);
        let queued = new Set();
        let takenSlots = new Set();

        inventorySlots.forEach((dbid, slot) => {
            if (slot >= equipSlotMin && targetIds.has(dbid)) {
                queued.add(dbid);
                takenSlots.add(slot);
            }
        });

        let toEquip = [];
        inventorySlots.forEach((dbid, slot) => {
            if (slot >= equipSlotMin) return;
            if (!targetIds.has(dbid)) return;
            if (queued.has(dbid)) return;

            let info = loadedItems.get(dbid);
            if (!info) return;

            let validSlots = itemEquipSlots[info.type];
            if (!validSlots) return;

            queued.add(dbid);

            let equipSlot = validSlots.find(s => !inventorySlots.has(s) && !takenSlots.has(s)) ?? validSlots.find(s => !takenSlots.has(s));

            if (equipSlot !== undefined) {
                takenSlots.add(equipSlot);
                toEquip.push({
                    from: slot,
                    to: equipSlot
                });
            }
        });

        if (toEquip.length === 0) {
            if (retry > config.maxRetryAmount) return;
            await delay(config.retryDelay);
            return gearSetEquip(retry++);
        }

        for (let {
                from,
                to
            }
            of toEquip) {
            moveItem(from, to);
            await delay(config.equipDelay);
        }
    }

    async function stashGearSetWithdraw() {
        let gearSet = selectedStashSet ? getSetByValue(selectedStashSet) : null;
        if (!gearSet) return;
        let targetIds = new Set(gearSet.items);
        let queued = new Set();
        for (let dbid of stashSlots) {
            if (!targetIds.has(dbid) || queued.has(dbid)) continue;
            if (findFirstEmptyBagSlot() === -1) break;
            queued.add(dbid);
            clientPlayerCommand("itemunstash", `${dbid}`);
            await delay(config.unstashDelay);
        }
    }

    async function stashGearSetDeposit() {
        let gearSet = selectedStashSet ? getSetByValue(selectedStashSet) : null;
        if (!gearSet) return;
        let targetIds = new Set(gearSet.items);
        let queued = new Set();
        let toStash = [];
        inventorySlots.forEach((dbid, slot) => {
            if (!targetIds.has(dbid) || queued.has(dbid)) return;
            let info = loadedItems.get(dbid);
            if (!config.stashCharms && info && info.type === "charm") return;
            queued.add(dbid);
            toStash.push(slot);
        });
        let takenSlots = new Set();
        for (let slot of toStash) {
            if (slot >= equipSlotMin) {
                let emptySlot = findFirstEmptyBagSlot(takenSlots);
                if (emptySlot === -1) continue;
                takenSlots.add(emptySlot);
                clientPlayerCommand("itemmove", `${slot} ${emptySlot}`);
                await delay(config.stashDelay / 2);
                clientPlayerCommand("itemstash", `${emptySlot}`);
                await delay(config.stashDelay / 2);
                await delay(config.stashDelay);
                continue;
            }
            clientPlayerCommand("itemstash", `${slot}`);
            await delay(config.stashDelay);
        }
    }

    function makeButton(text, cls, font, size) {
        let btn = document.createElement("button");
        btn.textContent = text;
        btn.className = cls;
        btn.style.font = font;
        btn.style.minWidth = size;
        return btn;
    }

    function makeOption(name, value) {
        let option = document.createElement("option");
        option.textContent = name;
        option.value = value;
        return option;
    }

    function fillSelect(selector, getSelected, setSelected) {
        let sets = getPlayerSets();
        while (selector.firstChild) selector.removeChild(selector.firstChild);
        for (let {
                name,
                value
            }
            of sets) selector.appendChild(makeOption(name, value));
        if (!getSelected() && sets.length) setSelected(sets[0].value);
        if (getSelected()) selector.value = getSelected();
    }

    function populateCharPanelSelect(selector) {
        fillSelect(selector, () => selectedGearSet, v => {
            selectedGearSet = v
        });
    }

    function populateStashSelect(selector) {
        fillSelect(selector, () => selectedStashSet, v => {
            selectedStashSet = v
        });
    }


    function injectGearSetUI(parent) {
        if (document.getElementById("gearSetRow")) return;

        let row = document.createElement("div");
        row.id = "gearSetRow";
        row.style.display = "flex";
        row.style.gap = "5px";
        row.style.alignItems = "center";
        row.style.padding = "2px 0";
        row.style.width = "100%";
        row.style.overflow = "hidden";

        let createBtn = makeButton("Create", "btn black textprimary", "bold 15px hordes", "70px");
        let deleteBtn = makeButton("Delete", "btn black textprimary", "bold 15px hordes", "70px");
        let equipBtn = makeButton("Equip", "btn black textgreen", "bold 15px hordes", "70px");

        let selector = document.createElement("select");
        selector.style.maxWidth = "100px";
        selector.style.font = "15px bold hordes";

        let nameInput = document.createElement("input");
        nameInput.type = "text";
        nameInput.placeholder = "Name";
        nameInput.className = "textwhite";
        nameInput.style.font = "bold 15px hordes";

        row.append(createBtn, deleteBtn, equipBtn, selector, nameInput);

        createBtn.addEventListener("click", gearSetCreate);
        deleteBtn.addEventListener("click", gearSetDelete);
        equipBtn.addEventListener("click", () => gearSetEquip());
        selector.addEventListener("change", e => {
            selectedGearSet = e.target.value || null
        });
        nameInput.addEventListener("input", e => {
            gearSetNameInput = e.target.value
        });

        parent.prepend(row);
        populateCharPanelSelect(selector);
    }

    function injectStashUI(formelements) {
        if (document.getElementById("stashGearSetRow")) return;

        let row = document.createElement("div");
        row.id = "stashGearSetRow";
        row.style.display = "flex";
        row.style.gap = "5px";
        row.style.alignItems = "center";
        row.style.order = "-1";
        row.style.marginRight = "auto";

        let withdrawBtn = makeButton("Withdraw", "btn green textblack", "bold 14px hordes", "95px");
        let depositBtn = makeButton("Deposit", "btn cyan textblack", "bold 14px hordes", "95px");

        let selector = document.createElement("select");
        selector.style.maxWidth = "100px";
        selector.style.font = "15px bold hordes";

        row.append(selector, withdrawBtn, depositBtn);

        withdrawBtn.addEventListener("click", stashGearSetWithdraw);
        depositBtn.addEventListener("click", stashGearSetDeposit);
        selector.addEventListener("change", e => {
            selectedStashSet = e.target.value || null
        });

        formelements.prepend(row);
        populateStashSelect(selector);
    }

    let alreadyRefreshing = false;

    function refreshUI() {
        if (alreadyRefreshing) return;
        alreadyRefreshing = true;
        requestAnimationFrame(() => {
            alreadyRefreshing = false;
            let row = document.getElementById("gearSetRow");
            if (row) {
                let nameField = row.querySelector("input");
                if (nameField) nameField.value = gearSetNameInput;
                let selector = row.querySelector("select");
                if (selector) populateCharPanelSelect(selector);
            }
            let stashRow = document.getElementById("stashGearSetRow");
            if (stashRow) {
                let selector = stashRow.querySelector("select");
                if (selector) populateStashSelect(selector);
            }
        });
    }

    const mutationObserver = new MutationObserver(() => {
        let equipSlots = document.getElementById("equipslots");
        if (!equipSlots) {
            document.getElementById("gearSetRow")?.remove();
            currentPlayerName = null;
        }
        if (equipSlots && parent && !document.getElementById("gearSetRow")) {
            injectGearSetUI(equipSlots.parentElement);
        }

        let _formelements = document.querySelector(".formelements");
        if (!_formelements) {
            document.getElementById("stashGearSetRow")?.remove();
            stashSlots.clear();
        }
        if (_formelements) {
            let formelements = document.querySelector(".panel-black.marg-top > .marg-top.formelements");
            if (formelements && !document.getElementById("stashGearSetRow")) {
                injectStashUI(formelements);
            }
        }
    });

    mutationObserver.observe(document.documentElement, {
        childList: true,
        subtree: true
    });
})();