gulper.io devtools + plugin loader

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

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==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();

})();