420's Shit Mod Menu (Unified Drag, Color + Size Picker, DOT OFFSET)

Drag menu+launcher together, color & size picker for crosshair dot, hide/close behaviors, no M key.

// ==UserScript==
// @name         420's Shit Mod Menu (Unified Drag, Color + Size Picker, DOT OFFSET)
// @version      10.0-CROSSHAIR-FIX
// @description  Drag menu+launcher together, color & size picker for crosshair dot, hide/close behaviors, no M key.
// @match        https://games.crazygames.com/en_US/bullet-force-multiplayer/*
// @match        https://www.multiplayerpiano.dev/*
// @match        http://localhost:48897/game
// @match        https://www.gamepix.com/play/bullet-force
// @match        https://www.miniplay.com/game/bullet-force-multiplayer
// @match        https://kbhgames.com/game/bullet-force
// @match        https://bullet-force.com/
// @match        https://www.jopi.com/game/game/bullet-force/
// @match        https://www.gogy.com/games/bullet-force
// @match        https://www.gameflare.com/online-game/bullet-force/
// @match        https://www.silvergames.com/en/bullet-force
// @match        https://kour-io.com/bullet-force
// @grant        none
// @run-at       document-idle
// @namespace https://greatest.deepsurf.us/users/1527535
// ==/UserScript==

(function () {
    'use strict';

    // ===========================
    //  SETTINGS (edit here)
    // ===========================

    // DO NOT CHANGE THESE CONSTANTS, THEY ARE USED FOR LOCAL STORAGE KEYS
    const LS_MODAL_LEFT = '420_modal_left';
    const LS_MODAL_TOP = '420_modal_top';
    const LS_MODAL_OPEN = '420_modal_open';
    const LS_CROSSHAIR_ON = '420_crosshair_on';
    const LS_CROSSHAIR_COLOR = '420_crosshair_color';
    const LS_CROSSHAIR_SIZE = '420_crosshair_size';
    const LS_CROSSHAIR_OFFSET_X = '420_crosshair_offset_x';
    const LS_CROSSHAIR_OFFSET_Y = '420_crosshair_offset_y';

    // Default values
    const DEFAULT_COLOR = '#ff0000'; // Default red
    const DEFAULT_SIZE = 5; // Default 5px size
    const DEFAULT_OFFSET = 0; // Default offset

    // Drag configuration
    const DRAG_THRESHOLD = 5;

    // Helper functions for Local Storage
    function writeLS(key, value) {
        try {
            localStorage.setItem(key, JSON.stringify(value));
        } catch (e) {
            console.error('Local storage write failed for key:', key, e);
        }
    }

    function readLS(key, defaultValue) {
        try {
            const stored = localStorage.getItem(key);
            if (stored === null) return defaultValue;
            return JSON.parse(stored);
        } catch (e) {
            console.error('Local storage read failed for key:', key, e);
            return defaultValue;
        }
    }

    // ===========================
    //  CROSSHAIR LOGIC
    // ===========================

    function createCrosshairIfNeeded() {
        let crosshair = document.getElementById('420-crosshair');
        if (!crosshair) {
            crosshair = document.createElement('div');
            crosshair.id = '420-crosshair';
            // Base styles: fixed position, centered
            crosshair.style.cssText = `
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                z-index: 999999;
                pointer-events: none;
            `;
            document.body.appendChild(crosshair);
        }
        return crosshair;
    }

    function updateCrosshairStyle(color, size) {
        const crosshair = createCrosshairIfNeeded();

        // Get stored values or use defaults
        const chColor = color || readLS(LS_CROSSHAIR_COLOR, DEFAULT_COLOR);
        const chSize = size || readLS(LS_CROSSHAIR_SIZE, DEFAULT_SIZE);
        const chOffsetX = readLS(LS_CROSSHAIR_OFFSET_X, DEFAULT_OFFSET);
        const chOffsetY = readLS(LS_CROSSHAIR_OFFSET_Y, DEFAULT_OFFSET);

        // Apply styles
        crosshair.style.width = `${chSize}px`;
        crosshair.style.height = `${chSize}px`;
        crosshair.style.borderRadius = '50%';
        crosshair.style.backgroundColor = chColor;
        crosshair.style.transform = `translate(calc(-50% + ${chOffsetX}px), calc(-50% + ${chOffsetY}px))`;

        // Update visibility based on the toggle state
        if (readLS(LS_CROSSHAIR_ON, true)) {
            crosshair.style.display = 'block';
        } else {
            crosshair.style.display = 'none';
        }
    }

    function toggleCrosshair(isVisible) {
        const crosshair = createCrosshairIfNeeded();
        crosshair.style.display = isVisible ? 'block' : 'none';
        writeLS(LS_CROSSHAIR_ON, isVisible);
    }

    // ===========================
    //  MODAL HTML GENERATION
    // ===========================

    function createModal() {
        const existingContainer = document.getElementById('420-modal-container');
        if (existingContainer) return;

        const container = document.createElement('div');
        container.id = '420-modal-container';
        // Get initial position from LS or use defaults
        const left = readLS(LS_MODAL_LEFT, window.innerWidth / 2 - 200);
        const top = readLS(LS_MODAL_TOP, window.innerHeight / 2 - 150);
        const isOpen = readLS(LS_MODAL_OPEN, false);

        container.style.cssText = `
            position: fixed;
            left: ${left}px;
            top: ${top}px;
            z-index: 1000000;
            user-select: none;
            display: ${isOpen ? 'block' : 'none'};
            font-family: Arial, sans-serif;
        `;

        // The '420's Shit Mod Menu' Modal Structure
        container.innerHTML = `
            <div id="420-modal-card" style="
                background-color: #333;
                border: 2px solid #0f0;
                color: #fff;
                width: 300px;
                box-shadow: 0 4px 15px rgba(0, 255, 0, 0.4);
                border-radius: 8px;
                padding: 10px;
                box-sizing: border-box;
                display: flex;
                flex-direction: column;
                cursor: grab; /* Enable dragging */
            ">
                <!-- Header (Draggable Area) -->
                <div id="420-modal-header" style="
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    margin-bottom: 10px;
                    padding-bottom: 5px;
                    border-bottom: 1px solid #555;
                ">
                    <h3 style="margin: 0; color: #0f0; font-size: 16px;">420's Shit Mod Menu</h3>
                    <!-- FIX: Added ID for close button and inline style -->
                    <button id="modal-close-button" style="
                        background: none;
                        border: none;
                        color: #f00;
                        font-size: 18px;
                        cursor: pointer;
                        padding: 0 5px;
                        line-height: 1;
                    ">X</button>
                </div>

                <!-- Content Area -->
                <div style="
                    display: flex;
                    flex-direction: column;
                    gap: 10px;
                    padding: 5px 0;
                    cursor: default; /* Reset cursor for content area */
                ">

                    <!-- Crosshair Toggle Button -->
                    <button id="crosshair-toggle-button" style="
                        background-color: #222;
                        color: #0f0;
                        border: 1px solid #0f0;
                        padding: 8px;
                        border-radius: 4px;
                        cursor: pointer;
                        font-weight: bold;
                        transition: background-color 0.2s;
                    ">
                        Toggle Crosshair (Current: ON)
                    </button>

                    <!-- Crosshair Color Picker -->
                    <div style="display: flex; justify-content: space-between; align-items: center;">
                        <label for="crosshair-color-picker" style="color: #ccc;">Crosshair Color:</label>
                        <input type="color" id="crosshair-color-picker" value="${readLS(LS_CROSSHAIR_COLOR, DEFAULT_COLOR)}" style="
                            padding: 0;
                            height: 25px;
                            width: 50px;
                            border: 1px solid #0f0;
                            background: none;
                            cursor: pointer;
                        ">
                    </div>

                    <!-- Crosshair Size Slider -->
                    <div style="display: flex; flex-direction: column; gap: 5px;">
                        <label for="crosshair-size-slider" style="color: #ccc; display: flex; justify-content: space-between;">
                            <span>Crosshair Size:</span>
                            <span id="crosshair-size-value">${readLS(LS_CROSSHAIR_SIZE, DEFAULT_SIZE)}px</span>
                        </label>
                        <input type="range" id="crosshair-size-slider" min="1" max="20" step="1" value="${readLS(LS_CROSSHAIR_SIZE, DEFAULT_SIZE)}" style="width: 100%; cursor: pointer;">
                    </div>

                    <!-- Other Mod Buttons (Fewer buttons as requested) -->
                    <button style="background-color: #555; color: #fff; border: none; padding: 8px; border-radius: 4px; cursor: pointer;">
                        Example Mod Button A
                    </button>
                    <button style="background-color: #555; color: #fff; border: none; padding: 8px; border-radius: 4px; cursor: pointer;">
                        Example Mod Button B
                    </button>
                </div>
            </div>
        `;

        document.body.appendChild(container);
    }

    // ===========================
    //  TOGGLE BUTTON FOR MODAL
    // ===========================

    function createLauncherButton() {
        const button = document.createElement('button');
        button.id = '420-launcher-button';
        button.textContent = '420 Menu';
        button.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            z-index: 1000000;
            background-color: #444;
            color: #0f0;
            border: 2px solid #0f0;
            padding: 10px 15px;
            border-radius: 6px;
            cursor: pointer;
            box-shadow: 0 0 10px rgba(0, 255, 0, 0.5);
            font-size: 14px;
            font-weight: bold;
            transition: background-color 0.2s, box-shadow 0.2s;
        `;

        button.addEventListener('click', () => {
            const modal = document.getElementById('420-modal-container');
            if (modal) {
                const isVisible = modal.style.display === 'none' || modal.style.display === '';
                modal.style.display = isVisible ? 'block' : 'none';
                writeLS(LS_MODAL_OPEN, isVisible);
            }
        });

        document.body.appendChild(button);
        return button;
    }

    // ===========================
    //  INITIALIZATION
    // ===========================

    window.addEventListener('load', () => {
        createModal();
        createLauncherButton();

        const container = document.getElementById('420-modal-container');
        const modalCard = document.getElementById('420-modal-card');
        const header = document.getElementById('420-modal-header');

        // --- Drag Logic Variables ---
        let isModalDragging = false;
        let startXModal, startYModal;
        let initialXModal, initialYModal;

        if (header) {
            header.addEventListener('mousedown', (e) => {
                if (e.button !== 0) return; // Only left click
                isModalDragging = true;
                startXModal = e.clientX;
                startYModal = e.clientY;
                initialXModal = modalCard.offsetLeft;
                initialYModal = modalCard.offsetTop;
                modalCard.style.cursor = 'grabbing';
            });
        }

        document.addEventListener('mousemove', (e) => {
            if (!isModalDragging) return;

            const dx = e.clientX - startXModal;
            const dy = e.clientY - startYModal;

            let newLeft = initialXModal + dx;
            let newTop = initialYModal + dy;

            // Basic boundary checks
            newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - modalCard.offsetWidth));
            newTop = Math.max(0, Math.min(newTop, window.innerHeight - modalCard.offsetHeight));

            modalCard.style.left = newLeft + 'px';
            modalCard.style.top = newTop + 'px';
            container.style.left = newLeft + 'px';
            container.style.top = newTop + 'px';
        });

        document.addEventListener('mouseup', () => {
            if (isModalDragging) {
                isModalDragging = false;
                modalCard.style.cursor = 'grab';

                // Save new position
                writeLS(LS_MODAL_LEFT, container.offsetLeft);
                writeLS(LS_MODAL_TOP, container.offsetTop);
            }
        });

        // ===================================
        //  MODAL AND CROSSHAIR CONTROLS SETUP
        // ===================================

        // 1. FIX: Close Button Listener (The 'X')
        const modalClose = document.getElementById('modal-close-button');
        if (modalClose) {
            modalClose.addEventListener('click', () => {
                container.style.display = 'none';
                writeLS(LS_MODAL_OPEN, false);
            });
        }

        // 2. Crosshair State and Initial Style Application
        const isCrosshairOn = readLS(LS_CROSSHAIR_ON, true);
        const crosshairToggleButton = document.getElementById('crosshair-toggle-button');

        const updateToggleButtonText = () => {
             const state = readLS(LS_CROSSHAIR_ON, true) ? 'ON' : 'OFF';
             crosshairToggleButton.textContent = `Toggle Crosshair (Current: ${state})`;
             crosshairToggleButton.style.backgroundColor = state === 'ON' ? '#070' : '#700';
        };

        if (crosshairToggleButton) {
            updateToggleButtonText();
            crosshairToggleButton.addEventListener('click', () => {
                const newState = !readLS(LS_CROSSHAIR_ON, true);
                toggleCrosshair(newState);
                updateToggleButtonText();
            });
        }

        // Initial application of crosshair style based on saved/default settings
        updateCrosshairStyle();

        // 3. Crosshair Color Picker Listener (Instant Update)
        const colorPicker = document.getElementById('crosshair-color-picker');
        if (colorPicker) {
            colorPicker.addEventListener('input', (e) => {
                const newColor = e.target.value;
                writeLS(LS_CROSSHAIR_COLOR, newColor);
                updateCrosshairStyle(newColor); // Update with new color, size/offset read from LS
            });
        }

        // 4. Crosshair Size Slider Listener (Instant Update)
        const sizeSlider = document.getElementById('crosshair-size-slider');
        const sizeValueSpan = document.getElementById('crosshair-size-value');
        if (sizeSlider) {
            sizeSlider.addEventListener('input', (e) => {
                const newSize = parseInt(e.target.value);
                writeLS(LS_CROSSHAIR_SIZE, newSize);
                sizeValueSpan.textContent = `${newSize}px`;
                updateCrosshairStyle(null, newSize); // Update with new size, color/offset read from LS
            });
        }
    });

})();