gulper.io devtools + plugin loader

Exposes object in window to interact with gulper.io internal game states with dynamic plugins

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         gulper.io devtools + plugin loader
// @description  Exposes object in window to interact with gulper.io internal game states with dynamic plugins
// @author       activetutorial
// @license      MIT
// @version      1.0
// @match        https://gulper.io/*
// @run-at       document-start
// @namespace https://greasyfork.org/users/1410961
// ==/UserScript==

(function () {
    const _window = Function("return this;")();


    // Plugin System Architecture


    class BasePlugin {
        constructor(manager) {
            this.manager = manager;
            this.isActive = false;
            this.name = "UnnamedPlugin";
        }

        toggle() {
            this.isActive = !this.isActive;
            if (this.isActive && this.onEnable) this.onEnable();
            if (!this.isActive && this.onDisable) this.onDisable();
        }

        // Lifecycle Hooks to be overridden by subclasses:
        // onEnable() {}
        // onDisable() {}
        // onKeyDown(key) {}
        // onNativeSetAngle(baseAngle, args) { return { angle: Number, preventDefault: Boolean }; }
        // onTick(delta, time, baseAngle) { return { angle: Number }; }
    }

    class PluginManager {
        constructor(devtools) {
            this.devtools = devtools;
            this.plugins = new Map();
        }

        register(pluginClass) {
            const plugin = new pluginClass(this);
            this.plugins.set(plugin.name, plugin);
            console.log(`[Devtools] Loaded plugin: ${plugin.name}`);
        }

        get(name) {
            return this.plugins.get(name);
        }

        getActivePlugins() {
            return Array.from(this.plugins.values()).filter(p => p.isActive);
        }

        handleKeyDown(key) {
            for (const plugin of this.plugins.values()) {
                if (plugin.onKeyDown) plugin.onKeyDown(key);
            }
        }
    }


    // Core Devtools


    class GulperDevtools {
        constructor() {
            this.bus = new EventTarget();
            this.observations = [];
            this.scene = null;
            this.game = null;
            this.captureNext = false;
            this.originals = {};

            // Core state for the central loop
            this.lastInputAngle = 0;
            this.lastFrameTime = performance.now();
            this.animationFrameId = null;

            this.plugins = new PluginManager(this);
        }

        init() {
            _window.__$3_DEVTOOLS__ = this.bus;
            _window.__devtools = this;

            this.bus.addEventListener("observe", (e) => this.handleObserve(e?.detail));

            // Quietly bypass video ads
            Object.defineProperty(_window, "start_video_ad", {
                get: () => () => { _window.on_video_ad_finished(true); },
                set: () => {}, // ignore silently
                configurable: false
            });

            // Bind global input
            window.addEventListener("keydown", (e) => this.plugins.handleKeyDown(e.key));

            // Start the central game loop
            this.animationFrameId = requestAnimationFrame((time) => this.tick(time));

            this.loadDefaultPlugins();
        }

        loadDefaultPlugins() {
            this.plugins.register(RotatePlugin);
            this.plugins.register(WigglePlugin);
        }

        handleObserve(payload) {
            if (!payload) return;

            const clone = { ...payload };
            this.observations.push([payload, clone]);

            if (this.captureNext) {
                this.scene = clone;
                this.captureNext = false;
            }

            if (payload?.domElement?.id === "aux-canvas") {
                this.captureNext = true;
            }

            // Hook the game state once we have enough observations
            if (this.observations.length === 10 && !this.game) {
                this.game = this.findGame(this.scene);
                if (this.game) {
                    this.patchSetAngle(this.game);
                    console.log("[Devtools] Game hooked:", this.game);
                }
            }
        }

        findGame(scene) {
            return Object.values(
                scene?.children
                ?.find(n => n.type === "OrthographicCamera")
                ?.children?.find(n => n.type === "Group") || {}
            )?.find(v => typeof v?.anims_on_freed === "function");
        }

        findSetAngle(game) {
            return Object.entries(game.constructor.prototype)
                .find(([_, f]) =>
                      typeof f === "function" &&
                      f.length === 2 &&
                      f.toString().includes("<<")
                     )?.[0];
        }

        patchSetAngle(game) {
            const name = this.findSetAngle(game);
            if (!name) return;

            const original = game.constructor.prototype[name];
            this.originals.setAngle = original;
            const self = this;

            game.constructor.prototype[name] = function (angle, ...args) {
                self.lastInputAngle = angle;

                let finalAngle = angle;
                let preventOriginal = false;

                // 1. Let plugins intercept/modify native mouse movements
                for (const plugin of self.plugins.getActivePlugins()) {
                    if (plugin.onNativeSetAngle) {
                        const result = plugin.onNativeSetAngle(finalAngle, args);
                        if (result) {
                            if (result.angle !== undefined) finalAngle = result.angle;
                            if (result.preventDefault) preventOriginal = true;
                        }
                    }
                }

                // 2. Apply the final resolved angle
                if (!preventOriginal) {
                    return original.apply(this, [finalAngle, ...args]);
                }
            };
        }

        // Central Loop: Handles automatic continuous updates when no mouse events are sent
        tick(time) {
            requestAnimationFrame((t) => this.tick(t));

            const delta = (time - this.lastFrameTime) / 1000;
            this.lastFrameTime = time;

            if (!this.game || !this.originals.setAngle) return;

            let finalAngle = this.lastInputAngle;
            let needsForcedUpdate = false;

            for (const plugin of this.plugins.getActivePlugins()) {
                if (plugin.onTick) {
                    const result = plugin.onTick(delta, time, finalAngle);
                    if (result && result.angle !== undefined) {
                        finalAngle = result.angle;
                        needsForcedUpdate = true;
                    }
                }
            }

            // Only push the update to the game if a plugin actively modified the angle
            if (needsForcedUpdate) {
                this.originals.setAngle.call(this.game, finalAngle, true);
            }
        }
    }


    // Plugins


    class RotatePlugin extends BasePlugin {
        constructor(manager) {
            super(manager);
            this.name = "RotateMode";
            this.angle = 0;
            this.SPEED = Math.PI; // radians per second
        }

        onKeyDown(key) {
            if (key === "c") this.toggle();
        }

        onNativeSetAngle(baseAngle, args) {
            // Block native mouse inputs entirely while spinning
            return { preventDefault: true };
        }

        onTick(delta) {
            // Continuously increment angle
            this.angle += delta * this.SPEED;
            return { angle: this.angle };
        }
    }

    class WigglePlugin extends BasePlugin {
        constructor(manager) {
            super(manager);
            this.name = "WiggleMode";
            this.time = 0;
            this.AMPLITUDE = 0.5;
            this.SPEED = 15;
        }

        onKeyDown(key) {
            if (key === "x") this.toggle();
        }

        getWiggleOffset() {
            return Math.sin(this.time * this.SPEED) * this.AMPLITUDE;
        }

        onNativeSetAngle(baseAngle, args) {
            // If Rotate is active, step back to prevent conflicts
            if (this.manager.get("RotateMode")?.isActive) return;

            // Apply wiggle offset to active mouse movements
            return { angle: baseAngle + this.getWiggleOffset() };
        }

        onTick(delta, absoluteTime, baseAngle) {
            if (this.manager.get("RotateMode")?.isActive) return;

            this.time += delta;

            // Continually push the wiggle offset to the last known mouse position
            return { angle: baseAngle + this.getWiggleOffset() };
        }
    }


    // Boot


    const devtools = new GulperDevtools();
    devtools.init();

})();