gulper.io devtools + plugin loader

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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

})();