Gear set manager

Hordes.io script

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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
    });
})();