Agma.io Tools - Fast Eject, Auto Re-spawn, Re-spawn on R, Quick buy

Fast eject, Auto Re-spawn, Re-spawn on R, Quick buy, Freeze on F, and more

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Agma.io Tools - Fast Eject, Auto Re-spawn, Re-spawn on R, Quick buy
// @namespace    http://tampermonkey.net/
// @version      2.52
// @description  Fast eject, Auto Re-spawn, Re-spawn on R, Quick buy, Freeze on F, and more
// @author       reagent
// @match        agma.io
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    let features = [{
        name: "Auto re-spawn",
        enabled: true,
        bootstrap: class {
            constructor() {
                this.playBtn = document.querySelector("#playBtn");
                this.advertContinue = document.querySelector("#advertContinue");
                this.playTimeout = null;
                this.lastClosed = 0;
                this.lastDisplay = "none";
                this.advertWatch = new MutationObserver(this.advertMutation.bind(this));
                this.overlayWatch = new MutationObserver(this.overlayMutation.bind(this));
                this.resumeGame = this.resumeGame.bind(this);
                this.clearAdvert = null;
                this.clearPlay = null;
            }
            closedByDeath() {
                return Date.now() - this.lastClosed < 300;
            }
            advertMutation(mutations) {
                for (let mutation of mutations) {
                    if (mutation.target.style.display !== this.lastDisplay) {
                        this.lastDisplay = mutation.target.style.display;

                        if (this.lastDisplay !== "none") {
                            if (this.playTimeout) window.clearTimeout(this.playTimeout);
                            this.playTimeout = window.setTimeout(this.resumeGame, 5);
                        }
                    }
                }
            }
            overlayMutation(mutations) {
                if (this.overlays.style.display === "none") {
                    this.lastClosed = Date.now();
                }
            }
            resumeGame() {
                if (this.closedByDeath() || !document.hasFocus()) return; // info window was open and closed by dying in the background
                if (!this.clearAdvert) return console.error("Advert continue function still not found");

                this.playTimeout = null;
                this.clearAdvert()
                window.closeAdvert();

                if (this.clearPlay) {
                    this.clearPlay();
                    this.playBtn.click();
                } else
                    return console.error("Play button continue function still not found");
            }

            load() {
                this.advert = document.querySelector("#advert");
                this.overlays = document.querySelector("#overlays");

                this.advertWatch.observe(this.advert, { attributes: true, attributeFilter: ["style"] });
                this.overlayWatch.observe(this.overlays, { attributes: true, attributeFilter: ["style"] });

                const playBtnSearch = globals.agma.getSearchString("#playBtn");
                const continueBtnSearch = globals.agma.getSearchString("#advertContinue");

                const removePlayListener = addTimeoutListener((fn, delay) => {
                    if (fn.toString().indexOf(playBtnSearch) !== -1 && delay > 1600) {
                        this.clearPlay = fn;
                        this.clearPlay();
                        removePlayListener();
                    }
                })

                const removeContinueListener = addTimeoutListener((fn, delay) => {
                    if (fn.toString().indexOf(continueBtnSearch) != -1 && delay > 1600) {
                        this.clearAdvert = fn;
                        removeContinueListener();
                    }
                })
            }

            unload() {
                this.advertWatch.disconnect();
                this.overlayWatch.disconnect();
            }
        }
    }, {
        name: "Uncensor input",
        enabled: true,
        bootstrap: class {
            constructor() {
                this.forbiddenWords = ["fuck", "shit", "ass", "dick", "penis", "dick", "pussy", "fag", "bitch", "sucker", "tits", "porn", "cunt", "cock"];
                this.override = window.getEventListeners("copy")[0];
                this.chatBox = document.querySelector("#chtbox");
                this.nicknameInput = document.querySelector("#nick");
                this.listener = this.listener.bind(this);
            }

            listener(event) {
                //if(event.which !== 13) return;

                const zeroWidth = String.fromCharCode(8203);
                const input = event.currentTarget;
                const text = input.value.toLowerCase();

                for (const forbidden of this.forbiddenWords) {
                    const i = text.indexOf(forbidden);

                    if (i !== -1) {
                        const found = input.value.substr(i, forbidden.length); // retain original to keep case
                        input.value = input.value.replace(found, found.split("").join(zeroWidth))
                    }
                }
            }

            load() {
                if (this.override) {
                    const { fn, capture } = this.override;
                    window.removeEventListener("copy", fn, capture)
                    window.removeEventListener("paste", fn, capture)
                    window.removeEventListener("cut", fn, capture)
                }
                this.chatBox.addEventListener("keydown", this.listener);
                this.nicknameInput.addEventListener("keydown", this.listener);
            }

            unload() {
                if (this.override) {
                    const { fn, capture } = this.override;
                    window.addEventListener("copy", fn, capture)
                    window.addEventListener("paste", fn, capture)
                    window.addEventListener("cut", fn, capture)
                }
                this.chatBox.removeEventListener("keydown", this.listener);
                this.nicknameInput.removeEventListener("keydown", this.listener);
            }
        }
    }, {
        name: "Fast Eject",
        enabled: true,
        bootstrap: class {
            constructor() {
                this.keyDown = false;
                this.syntheticEvent = Object.freeze({ keyCode: 87, synthetic: true });
                this.pressW = this.pressW.bind(this);
                this.downListener = this.downListener.bind(this);
                this.upListener = this.upListener.bind(this);
            }
            pressW() {
                window.onkeydown(this.syntheticEvent);
                window.onkeyup(this.syntheticEvent);
                if (this.keyDown) window.setTimeout(this.pressW, 25);
            }
            downListener(event) {
                if (event.keyCode === 87 && !event.synthetic) {
                    if (this.keyDown) return;
                    this.keyDown = true;
                    window.setTimeout(this.pressW, 25);
                    window.onkeyup(this.syntheticEvent); // complete first press
                }
            }
            upListener(event) {
                if (event.keyCode === 87 && !event.synthetic) {
                    this.keyDown = false;
                }
            }
            load() {
                window.addEventListener("keydown", this.downListener);
                window.addEventListener("keyup", this.upListener);
            }
            unload() {
                window.removeEventListener("keydown", this.downListener);
                window.removeEventListener("keyup", this.upListener);
            }
        }
    }, {
        name: "Re-spawn on R",
        enabled: true,
        bootstrap: class {
            constructor() {
                this.nicknameInput = document.querySelector("#nick");
                this.listener = this.listener.bind(this);
            }
            listener(event) {
                if (event.keyCode === 82 && globals.isGameActive()) {
                    window.rspwn(this.nicknameInput.value);
                }
            }
            load() {
                window.addEventListener("keydown", this.listener);
            }

            unload() {
                window.removeEventListener("keydown", this.listener);
            }
        }

    }, {
        name: "New identity on N | Skin on S",
        enabled: true,
        bootstrap: class {
            constructor() {
                this.nickInput = document.querySelector("#nick");
                this.overlays = document.querySelector("#overlays");
                this.nicks = [];
                this.skins = [];
                this.lastRespawn = 0;
                this.pendingId = false;
                this.pendingSkin = false;
                this.listener = this.listener.bind(this);
                this.hookFillText = this.hookFillText.bind(this);
                this.addNick = this.addNick.bind(this);
            }
            randNum(min, max) {
                return Math.floor(Math.random() * (max - min) + min);
            }
            rankUsername(username) {
                return username
                    .split("")
                    .reduce((cur, char) => cur + (char.charCodeAt(0) > 255 ? 3 : 1), 0)
            }
            sortAndTrim(list) {
                return list
                    .map(username => [this.rankUsername(username), username])
                    .sort(([rankA], [rankB]) => rankA - rankB)
                    .slice(0, 60)
                    .map(([_, username]) => username);
            }
            addNick(name) {
                if (name.length > 3 && this.nicks.indexOf(name) === -1) {
                    this.nicks.push(name);
                    this.nicks = this.sortAndTrim(this.nicks);
                }
            }
            changeSkin() {
                if (this.pendingSkin) return Promise.reject("Skin pending");
                if (this.skins.length) {
                    this.pendingSkin = true;
                    return globals.overlay.open(true)
                        .then(() => window.toggleSkin(this.skins[this.randNum(0, this.skins.length)]))
                        .then(() => globals.overlay.close())
                        .then(() => this.pendingSkin = false)
                } else {
                    return globals.message.error("No skins available, are you logged in?");
                }
            }
            changeNick() {
                this.pendingId = true;
                return new Promise((resolve, reject) => {
                    let newNick = "";
                    for (let i = 0; i < 5; i++) {
                        newNick = this.nicks[this.randNum(0, this.nicks.length)];
                        if (newNick !== this.nickInput.value) break;
                    }
                    this.nickInput.value = newNick

                    globals.message.show("Switching to: " + newNick)
                        .then(resolve)
                        .catch(reject)
                }).then(() => this.pendingId = false)
            }
            listener(event) {
                if (!globals.isGameActive()) return;
                if (event.keyCode === 78) {
                    Promise.all([this.changeSkin(), this.changeNick()]).then(() => {
                        console.log(this.nicks);
                        window.partyDecline();
                        window.rspwn(this.nickInput.value);
                    }).catch(() => { })
                } else if (event.keyCode === 73) {
                    this.changeSkin();
                }
            }

            hookFillText() {
                const origFillText = CanvasRenderingContext2D.prototype.fillText;
                const self = this;
                CanvasRenderingContext2D.prototype.fillText = function () {
                    if (this.canvas.id === "leaderboard") {
                        const item = arguments[0];
                        const start = item.indexOf(". ");
                        if (start !== -1) {
                            const name = item.substring(start + 2);
                            self.addNick(name);
                        }
                    } else if (this.canvas.height === 23) {
                        const item = arguments[0];
                        if (typeof item === "string" && this.fillStyle !== "#f5f6ce" && item !== "Agma.io") {
                            self.addNick(item)
                        }
                    }
                    return origFillText.apply(this, arguments);
                }
                return () => CanvasRenderingContext2D.prototype.fillText = origFillText;
            }
            hookRespawn() {
                const self = this;
                const rspwn = window.rspwn;

                window.rspwn = function () {
                    if (globals.message.rejectLast) {
                        globals.message.rejectLast();
                        globals.message.rejectLast = null;
                    }
                    self.lastRespawn = Date.now();
                    rspwn.apply(this, arguments);
                }
                return () => window.rspwn = rspwn;
            }
            getSkins() {
                const pages = document.querySelectorAll("#skinsCustom [id^=publicSkinsPageContent]:not(#publicSkinsPageContentNew):not(#publicSkinsPageContentPopular)");
                if (!pages.length) return [];
                return Array.from(pages)
                    .map(el => el.textContent.match(/toggleSkin\color{#fff}{([0-9]+)}([0−9]+)/g))
                    .map(matches => matches.map(match => match.substring(11, match.length - 1)))
                    .flat();
            }
            loadSkins() {
                let tries = 0;

                globals.modals.hideAll()
                window.showSkin();

                return new Promise(resolve => {
                    const retry = () => {
                        const skins = this.getSkins();
                        if (skins.length || tries++ > 5) {
                            globals.modals.closeCurrent().then(() => globals.modals.showAll());
                            resolve(skins);
                        } else {
                            window.setTimeout(retry, 2000);
                        }
                    }
                    window.setTimeout(retry, 3000);
                })
            }
            load() {
                globals.status.onLogin(() => this.loadSkins().then(skins => {
                    this.skins = skins;
                }))
                globals.status.onLogout(() => this.skins = []);

                window.addEventListener("keydown", this.listener);
                this.unhookRespawn = this.hookRespawn();
                this.unhookFillText = this.hookFillText();
            }
            unload() {
                window.removeEventListener("keydown", this.listener);
                this.unhookFillText();
                this.unhookRespawn();
            }
        }
    }, {
        name: "Quick-buy powerups with keys 1-4", // rename later
        enabled: true,
        bootstrap: class {
            constructor() {
                this.confirmButton = null;
                this.alert = null;
                this.watcher = new MutationObserver(this.onMutations.bind(this));
                [this.buyRecombine, this.buySpeed, this.buyGrowth, this.buyPushEnemies] = document.querySelectorAll(".purchase-btn.confirmation");
                this.listener = this.listener.bind(this);
            }
            onAlertAvailable() {
                const showingNow = this.alert.classList.contains("showSweetAlert");

                if (showingNow) {
                    this.confirmButton = this.confirmButton || this.alert.querySelector("button.confirm");
                    if (!this.confirmButton) return;

                    // SweetAlert will ignore all clicks until this class is added
                    // which it waits nearly a full second to add.. annoying. Wasted a lot of time debugging this.
                    // https://github.com/lipis/bootstrap-sweetalert/blob/67fdf993b35fa0a9e2c2a34d218cc9d83a59b8bd/dev/modules/handle-click.js#L42
                    this.alert.classList.add("visible");
                    this.confirmButton.click();
                }
            }
            onMutations(mutations) {
                for (const mutation of mutations) {
                    if (mutation.type === "attributes") {
                        if (mutation.target === this.alert) {
                            this.onAlertAvailable();
                        }
                    } else if (mutation.type === "childList") {
                        for (const node of mutation.addedNodes) {
                            if (node.nodeType !== Node.ELEMENT_NODE) continue;
                            if (node.classList.contains("sweet-alert")) {
                                this.alert = node;
                                this.onAlertAvailable();
                                this.watcher.disconnect();
                                this.watcher.observe(node, { attributes: true, attributeFilter: ["class"] })
                            }
                        }
                    }
                }
            }
            listener(event) {
                if (globals.isGameActive()) { // focus is on the game rather than chat or anything else
                    if (event.keyCode === 49 || event.keyCode === 97) {
                        this.buyRecombine.click();
                    } else if (event.keyCode === 50 || event.keyCode === 98) {
                        this.buySpeed.click();
                    } else if (event.keyCode === 51 || event.keyCode === 99) {
                        this.buyGrowth.click();
                    } else if (event.keyCode === 52 || event.keyCode === 100) {
                        this.buyPushEnemies.click();
                    }
                }
            }

            load() {
                this.watcher.observe(document.body, {
                    childList: true
                });
                window.addEventListener("keydown", this.listener);
            }
            unload() {
                this.watcher.disconnect();
                window.removeEventListener("keydown", this.listener);
            }
        }
    }, {
        name: "Freeze on F | Fixate cursor on X",
        enabled: true,
        bootstrap: class {
            constructor() {
                this.release = this.release.bind(this);
                this.listener = this.listener.bind(this);
                this.capturePos = this.capturePos.bind(this);
                this.captureInt = null;
                this.freezing = false;
                this.fixating = false;
                this.currentX = Math.floor(window.innerWidth / 2);
                this.currentY = Math.floor(window.innerHeight / 2);
                //this.overlay = this.createOverlay();
            }
            createOverlay() {
                // This overlay is redundant now that we just splice into the mousemove event
                // Still I'm leaving just in case i want to reimplement it
                const overlay = document.createElement("div");
                overlay.style.position = "fixed";
                overlay.style.width = "100%";
                overlay.style.height = "100%";
                overlay.style.display = "none";
                overlay.style.zIndex = "100";
                overlay.addEventListener("mousemove", event => this.capturePos(event) && event.stopPropagation());
                overlay.addEventListener("mouseenter", event => event.stopPropagation());
                overlay.addEventListener("click", this.release);

                return overlay;
            }
            freeze(delay = 5, complete = false) {
                let iterations = complete ? 30 : 0;
                this.freezing = true;

                this.captureInt = window.setInterval(() => {
                    iterations++;
                    const targetX = window.innerWidth / 2;
                    const targetY = window.innerHeight / 2;

                    document.body.dispatchEvent(new MouseEvent("mousemove", { // simulate mouse slowly moving to center
                        clientX: this.currentX + (((targetX - this.currentX) / 30) * Math.min(iterations, 30)),
                        clientY: this.currentY + (((targetY - this.currentY) / 30) * Math.min(iterations, 30)),
                        detail: 0x0a
                    }))
                    if (delay === 5 && iterations > 500) {
                        window.clearInterval(this.captureInt);
                        this.freeze(100, true); // start over with reduced frequency
                    }
                }, delay);

                //this.overlay.style.display = "block";
            }
            release() {
                window.clearInterval(this.captureInt);
                this.freezing = false;
                this.fixating = false;
                this.captureInt = null;
                //this.overlay.style.display = "none";
            }
            fixate() {
                this.fixating = true;

                const targetX = this.currentX;
                const targetY = this.currentY;

                this.captureInt = window.setInterval(() => {
                    document.body.dispatchEvent(new MouseEvent("mousemove", {
                        clientX: targetX,
                        clientY: targetY,
                        detail: 0x0a
                    }));
                }, 100);

                //this.overlay.style.display = "block";
            }
            capturePos(event) {
                this.currentX = event.clientX;
                this.currentY = event.clientY;
                return true
            }
            listener(event) {
                if (!globals.isGameActive()) return;

                if (event.keyCode === 70) {
                    if (this.freezing) {
                        this.release();
                    } else {
                        if (this.fixating) this.release();
                        this.freeze();
                        globals.message.show("Frozen at current spot. Press F to unlock");
                    }
                } else if (event.keyCode === 88) {
                    if (this.fixating) {
                        this.release();
                    } else {
                        if (this.freezing) this.release();
                        this.fixate();
                        globals.message.show("Locked at current direction. Press X to unlock");
                    }
                }
            }
            hookMouseMove() {
                let orig;
                const self = this;
                // We watch for the "mousedown" event only because we know agma immediately sets it after mousemove
                // If we try to hook into mousemove immediately, it will be undefined
                window.awaitEventListener("mousedown").then(() => {
                    orig = document.body.onmousemove;
                    document.body.onmousemove = document.body.onmouseenter = function (event) {
                        if ((!self.freezing && !self.fixating) || event.detail === 0x0a) {
                            orig.apply(this, arguments);
                        }
                    }
                })
                return () => document.body.onmousemove = document.body.onmouseenter = orig;
            }
            load() {
                window.addEventListener("keydown", this.listener);
                window.addEventListener("mousemove", this.capturePos);
                this.unhookMouseMove = this.hookMouseMove()
            }
            unload() {
                window.removeEventListener("keydown", this.listener);
                window.removeEventListener("mousemove", this.capturePos);
                this.unhookMouseMove();
            }
        }
    }, {
        enabled: true,
        name: "Remove popups",
        bootstrap: function () {
            this.load = () => {
                const popups = document.querySelectorAll("body .modal");
                for (const popup of popups) {
                    if (popup.textContent.indexOf("referral") !== -1) {
                        popup.remove();
                    }
                }
                // const minionui = document.querySelector("#minionUi");
                // minionui && minionui.remove();
                // setTimeout(() => document.querySelectorAll("iframe").forEach(iframe => iframe.remove()), 3000);
            }
            this.unload = () => {

            }
        }
    }];
    const globals = {
        Modals: class {
            constructor() {
                this.modals = document.querySelectorAll(".modal");
            }
            getCurrent() {
                const backdrop = document.querySelector(".modal-backdrop");
                const modal = backdrop.parentElement;
                return modal;
            }
            closeCurrent() {
                const modal = this.getCurrent();
                return new Promise(resolve => {
                    const onClose = () => {
                        window.$(modal).off("hidden.bs.modal", onClose);
                        resolve();
                    }
                    window.$(modal).on("hidden.bs.modal", onClose)
                    window.$(modal).modal("hide");
                })
            }
            hideAll() {
                this.modals.forEach(modal => modal.classList.add("force-hide"));
            }
            showAll() {
                this.modals.forEach(modal => modal.classList.remove("force-hide"));
            }
        },
        Overlay: class {
            constructor() {
                this.openResolvers = new Map();
                this.closeResolvers = new Map();
                this.overlayOpen = true;
                this.covert = false;
                this.playBtn = document.querySelector("#playBtn");
                this.overlays = document.querySelector("#overlays");
                this.watchOverlay();
            }
            watchOverlay() {
                const observer = new MutationObserver(mutations => {
                    if (overlays.style.display === "" || overlays.style.display === "block" && overlays.style.opacity === "") {
                        if (this.overlayOpen) return;
                        this.overlayOpen = true;
                        this.onOpen();
                    } else {
                        if (!this.overlayOpen) return;
                        this.overlayOpen = false;
                        this.onClose();
                    }
                });
                observer.observe(overlays, {
                    attributes: true,
                    attributeFilter: ["style"]
                })
            }
            onOpen() {
                for (const [fn] of this.openResolvers.entries()) {
                    fn()
                    this.openResolvers.delete(fn);
                }
            }
            onClose() {
                for (const [fn] of this.closeResolvers.entries()) {
                    fn();
                    this.closeResolvers.delete(fn);
                }

                if (this.covert) {
                    this.overlays.classList.remove("force-hide");
                    this.covert = false;
                }
            }
            open(covert) {
                return new Promise(resolve => {
                    if (this.overlayOpen) return resolve();
                    this.openResolvers.set(resolve, null);

                    if (covert) {
                        this.overlays.classList.add("force-hide");
                        this.covert = true;
                    } else {
                        this.covert = false;
                    }

                    window.azad(true);
                })
            }
            close() {
                return new Promise(resolve => {
                    if (!this.overlayOpen) return resolve();
                    this.closeResolvers.set(resolve, null);
                    this.playBtn.removeAttribute("disabled");
                    this.playBtn.click();

                    if (this.covert) {
                        this.overlays.style.display = "none";
                    }
                })
            }
        },
        Status: class {
            constructor() {
                this.loggedIn = false;
                this.loginCallbacks = [];
                this.logoutCallbacks = [];
                this.watchPanel();
            }
            watchPanel() {
                const panel = document.querySelector("#dashPanel");
                const observer = new MutationObserver(mutations => {
                    if (panel.style.display === "" || panel.style.display === "block") {
                        if (this.loggedIn) return;
                        this.loggedIn = true;
                        for (const fn of this.loginCallbacks)
                            fn()
                    } else if (panel.style.display === "none") {
                        if (!this.loggedIn) return;
                        this.loggedIn = false;
                        for (const fn of this.logoutCallbacks)
                            fn();
                    }
                });
                observer.observe(panel, {
                    attributes: true,
                    attributeFilter: ["style"]
                })
            }

            onLogin(fn) {
                this.loginCallbacks.push(fn);
                if (this.loggedIn) fn();
            }
            onLogout(fn) {
                this.logoutCallbacks.push(fn)
            }
        },
        Message: class {
            constructor() {
                this.messageBar = document.querySelector("#curser");
            }
            error(message, expires = 5000) {
                this.messageBar.style.color = "rgb(255, 0, 0);"
                return this._show(message, expires);
            }
            show(message, expires = 5000) {
                this.messageBar.style.color = "rgb(0, 192, 0);"
                return this._show(message, expires)
            }
            _show(message, expires = 5000) {
                return new Promise((resolve, reject) => {
                    this.messageBar.textContent = message;

                    if (this.rejectLast) {
                        this.rejectLast();
                    }

                    this.messageBar.style.display = "block";
                    const messageTimeout = window.setTimeout(() => {
                        this.rejectLast = null;
                        this.messageBar.style.display = "none";
                        resolve()
                    }, expires)

                    this.rejectLast = () => {
                        this.messageBar.style.display = "none";
                        window.clearInterval(messageTimeout);
                        reject();
                    }
                })
            }
        },
        Agma: class {
            constructor() {
                this.globalKey = this.getGlobalKey();
                this.setGlobals();
            }
            setGlobals() {
                window.canRunAds=true;
            }
            getGlobalKey() {
                for (let key in window) {
                    if (key.indexOf("_0x") === 0
                        && window[key] instanceof Array
                        && window[key].length > 500)
                        return key;
                }
            }
            getSearchString(str) {
                const i = window[this.globalKey].indexOf(str);
                if (i === -1) return "";
                return this.globalKey + "[" + i + "]";
            }
        },
        isGameActive: () => {
            globals.playBtn = globals.playBtn || document.querySelector("#playBtn");
            return document.activeElement === document.body || document.activeElement === globals.playBtn;
        }
    }
    const hookTimeout = (window) => {
        const originalTimeout = window.setTimeout.bind(window);
        const awaiting = [];
        const nextTimeout = (function (handler, timeout) {
            for (let listener of awaiting) {
                listener(handler, timeout);
            }
            originalTimeout(handler, timeout);
        });

        const addTimeoutListener = (listener) => {
            if (window.setTimeout !== nextTimeout) {
                window.setTimeout = nextTimeout;
            }
            awaiting.push(listener);
            return () => {
                awaiting.splice(awaiting.indexOf(listener), 1);
                if (!awaiting.length) {
                    window.setTimeout = originalTimeout;
                }
            }
        }

        return [() => window.setTimeout = originalTimeout, addTimeoutListener]
    }
    const hookEvents = (EventTarget) => {
        const originalAdd = EventTarget.prototype.addEventListener;
        const originalRemove = EventTarget.prototype.removeEventListener;

        EventTarget.prototype.addEventListener = function () {
            originalAdd.apply(this, arguments); // run first to allow native method to halt execution in case of errors
            let [name, fn, capture] = arguments;
            capture = !!capture
            if (!this.eventListeners) this.eventListeners = [];
            this.eventListeners.push({ name, fn, capture });
            if (this.awaiting) {
                const i = this.awaiting.findIndex(({ eventName }) => eventName === name);
                if (i !== -1) {
                    this.awaiting[i].resolve();
                    this.awaiting.splice(i, 1);
                }
            }
        }

        EventTarget.prototype.removeEventListener = function () {
            originalRemove.apply(this, arguments);
            let [_name, _fn, _capture] = arguments;
            _capture = !!_capture;
            this.eventListeners = this.eventListeners ? this.eventListeners.filter(({ name, fn, capture }) => !(name === _name && fn === _fn && capture === _capture)) : [];
        }

        EventTarget.prototype.getEventListeners = function (eventName) {
            return this.eventListeners ? this.eventListeners.filter(({ name }) => name === eventName) : []
        }

        EventTarget.prototype.awaitEventListener = function (eventName) {
            return new Promise(resolve => {
                const events = this.getEventListeners(eventName);
                if (events.length) {
                    resolve();
                } else {
                    if (!this.awaiting) this.awaiting = [];
                    this.awaiting.push({ eventName, resolve })
                }
            })
        }

        return () => {
            EventTarget.prototype.addEventListener = originalAdd;
            EventTarget.prototype.removeEventListener = originalRemove;
            delete EventTarget.prototype.getEventListeners;
        }
    }
    const hookStyle = () => {
        const style = document.createElement("style")
        style.appendChild(document.createTextNode(".force-hide{display: none !important;}"));

        document.documentElement.appendChild(style);
        return () => document.documentElement.removeChild(style);
    }
    const hookProtections = () => {
        const orig = Document.prototype.createElement;
        Document.prototype.createElement = function (tag) {
            if (tag.toLowerCase() === "iframe") {
                tag = "div";
            }
            return orig.call(this, tag);
        }

        // if push comes to shove, i guess we could also just modify jQuery's selector fn
        // or the underlying document function calls
        // so they return a different iframe that we control

        Object.freeze(Document.prototype);
        Object.freeze(window.EventTarget.prototype);
        Object.freeze(window.EventTarget);
        Object.freeze(window.MutationObserver.prototype);
        Object.freeze(window.MutationObserver)
    }
    const init = () => {
        for (const global in globals) {
            if (global.charAt(0).toLowerCase() !== global.charAt(0))
                globals[global.toLowerCase()] = new globals[global]();
        }
        for (const feature of features) {
            if (feature.enabled) {
                feature.instance = new feature.bootstrap();
                feature.instance.load();
            }
        }
    }

    const hookTimeouts = () => {
        // Dear Agma admin, can we stop this arms race and just let people have fun?
        const timeoutListeners = [hookTimeout(window)];
        const onIframe = (frame) => {
            if (frame.src.indexOf("agma.io") === -1 || frame.contentWindow.hooked) return;
            timeoutListeners.push(hookTimeout(frame.contentWindow))
            hookEvents(frame.contentWindow.EventTarget);
            frame.contentWindow.hooked = true;
        }

        const observer = new MutationObserver(muts =>
            muts.forEach(mut => {
                if (mut.target.nodeType === Node.ELEMENT_NODE) {
                    if (mut.target.localName === "iframe") {
                        onIframe(mut.target);
                    } else {
                        document.querySelectorAll("iframe").forEach(frame => onIframe(frame));
                    }
                }
            })
        );

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

        const addTimeoutListener = (fn) => {
            const listeners = [];
            for (const [_, addTimeoutListener] of timeoutListeners) {
                listeners.push(addTimeoutListener(fn));
            }
            return () => listeners.forEach(remover => remover());
        }
        const unhook = () => {
            observer.disconnect();
            timeoutListeners.forEach(unhooker => unhooker());
        }

        return [addTimeoutListener, unhook]
    }

    const unhookEvents = hookEvents(window.EventTarget);
    const unhookStyle = hookStyle();
    const [addTimeoutListener, unhookTimeouts] = hookTimeouts();
    hookProtections();

    if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
        init();
    } else {
        window.addEventListener("DOMContentLoaded", init);
    }
})();