Arras.io Alt Movement Mimic (Autodetect)

Right-click on an alt to copy its movement, aiming, and shooting, with automatic variable detection.

当前为 2025-11-25 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Arras.io Alt Movement Mimic (Autodetect)
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Right-click on an alt to copy its movement, aiming, and shooting, with automatic variable detection.
// @author       danny
// @match        https://arras.io/*
// @grant        none
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // =========================================================================
    // GLOBAL GAME VARIABLE POINTERS (Will be set by auto-detection)
    // =========================================================================
    let gameVars = {
        canvas: null,
        myInputObject: null, 
        allTanksArray: null,
        camera: { x: 0, y: 0 } // Placeholder for camera position
    };
    
    let isInitialized = false;
    let isMimicking = false;
    let targetTankIndex = -1; 
    let notificationElement = null;

    // Fixed configuration
    const CANVAS_SELECTOR = "canvas:not([id])";
    const SEARCH_RADIUS = 150; // Max search distance in world units (e.g., pixels)
    const PLAYER_DIMENSION_THRESHOLD = 50; // Tanks are likely larger than this value

    /**
     * Creates a simple, non-blocking notification on the screen.
     * @param {string} message The message to display.
     */
    function showNotification(message) {
        if (!notificationElement) {
            notificationElement = document.createElement('div');
            notificationElement.style.cssText = `
                position: fixed;
                top: 10px;
                right: 10px;
                padding: 10px 15px;
                background: rgba(0, 0, 0, 0.8);
                color: #fff;
                font-family: sans-serif;
                font-size: 14px;
                border-radius: 5px;
                z-index: 10000;
                transition: opacity 0.3s ease-out;
                pointer-events: none;
                box-shadow: 0 4px 8px rgba(0,0,0,0.2);
            `;
            document.body.appendChild(notificationElement);
        }
        
        notificationElement.textContent = message;
        notificationElement.style.opacity = 1;

        // Automatically hide after 3 seconds
        clearTimeout(notificationElement.timeout);
        notificationElement.timeout = setTimeout(() => {
            notificationElement.style.opacity = 0;
        }, 3000);
    }

    // Utility function to approximate distance between two points
    function distance(x1, y1, x2, y2) {
        return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
    }

    /**
     * Converts screen coordinates (mouse) to game world coordinates 
     * using the dynamically found camera position.
     * @param {number} screenX 
     * @param {number} screenY 
     * @returns {object} { wx, wy } in world space.
     */
    function screenToWorldCoords(screenX, screenY) {
        const { canvas, camera } = gameVars;
        if (!canvas) return { wx: 0, wy: 0 };

        const centerX = canvas.width / 2;
        const centerY = canvas.height / 2;

        // The game's camera position (camera.x, camera.y) is the world center 
        // that corresponds to the screen center (centerX, centerY).
        const worldX = camera.x + (screenX - centerX);
        const worldY = camera.y + (screenY - centerY);
        
        return { wx: worldX, wy: worldY };
    }

    /**
     * Finds the closest tank index to the current world coordinates, excluding the main player.
     * @param {number} worldX Mouse X in game world coordinates
     * @param {number} worldY Mouse Y in game world coordinates
     * @returns {number} The index of the closest tank, or -1 if none found.
     */
    function findTargetTank(worldX, worldY) {
        let closestIndex = -1;
        let minDistance = Infinity;
        const tanks = gameVars.allTanksArray;

        if (!tanks || !Array.isArray(tanks)) return -1;

        // 1. First, try to find the player's tank index (the tank at the center)
        let playerTankIndex = -1;
        for (let i = 0; i < tanks.length; i++) {
            const tank = tanks[i];
            // Player tank is usually centered on the camera position
            if (tank && distance(tank.x, tank.y, gameVars.camera.x, gameVars.camera.y) < 1) { 
                 // Also confirm it looks like a player tank (large dimensions)
                if (tank.size > PLAYER_DIMENSION_THRESHOLD || tank.dimension > PLAYER_DIMENSION_THRESHOLD) {
                    playerTankIndex = i;
                    break;
                }
            }
        }

        // 2. Search for the closest nearby tank (alt)
        for (let i = 0; i < tanks.length; i++) {
            if (i === playerTankIndex) continue; // Skip the main player tank
            
            const tank = tanks[i];
            
            // Check if tank object has position data
            if (tank && tank.x !== undefined && tank.y !== undefined) {
                const dist = distance(worldX, worldY, tank.x, tank.y); 

                if (dist < minDistance && dist < SEARCH_RADIUS) {
                    minDistance = dist;
                    closestIndex = i;
                }
            }
        }
        return closestIndex;
    }
    
    /**
     * Aggressively searches the global 'window' scope for game variables based on object structure.
     * This function is the core of the auto-detection.
     */
    function findGameVariables() {
        const globalScope = window;
        
        // 1. Find Canvas
        gameVars.canvas = document.querySelector(CANVAS_SELECTOR);
        if (!gameVars.canvas) {
            console.warn("Autodetect: Canvas not found.");
            return false;
        }

        // 2. Find Tank Array and Camera/Input Objects
        // We will search for a few common patterns in global variables
        for (const prop in globalScope) {
            if (globalScope.hasOwnProperty(prop)) {
                const value = globalScope[prop];

                // --- Input Object Detection ---
                // An object that has common input keys (up, down, shoot)
                if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
                    if (value.up !== undefined && value.down !== undefined && value.shoot !== undefined) {
                        gameVars.myInputObject = value;
                        console.log(`Autodetect: Found Input Object at window.${prop}`);
                    }
                }

                // --- Tank Array Detection ---
                // A large array of objects with x, y, and size/dimension properties
                if (Array.isArray(value) && value.length > 5 && gameVars.allTanksArray === null) {
                    const sample = value[0];
                    if (typeof sample === 'object' && sample !== null &&
                        sample.x !== undefined && sample.y !== undefined && 
                        (sample.size !== undefined || sample.dimension !== undefined)) {
                        
                        gameVars.allTanksArray = value;
                        console.log(`Autodetect: Found Tank Array at window.${prop}`);
                    }
                }

                // --- Camera Position Detection ---
                // An object that has a position property (x, y) that frequently changes (camera position)
                if (typeof value === 'object' && value !== null && gameVars.camera.x === 0 && gameVars.camera.y === 0) {
                     if (value.x !== undefined && value.y !== undefined && typeof value.x === 'number') {
                         // Check if this object contains properties common to a camera/player state
                         if (value.vx !== undefined || value.vy !== undefined || value.size !== undefined) {
                             gameVars.camera = value;
                             console.log(`Autodetect: Found Camera/Player State at window.${prop}`);
                         }
                     }
                }
            }
        }
        
        // Final check for required variables
        if (gameVars.myInputObject && gameVars.allTanksArray) {
            console.log("Autodetect: All necessary game variables found.");
            return true;
        } else {
            console.warn("Autodetect: Still missing variables.", { Input: !!gameVars.myInputObject, Tanks: !!gameVars.allTanksArray });
            return false;
        }
    }


    /**
     * Main Mimic Loop: Runs periodically to copy the target's state.
     */
    function mimicLoop() {
        if (!isMimicking || targetTankIndex === -1 || !gameVars.myInputObject || !gameVars.allTanksArray) {
            return;
        }

        const targetTank = gameVars.allTanksArray[targetTankIndex];
        const myInput = gameVars.myInputObject;

        // Ensure the target tank still exists at this index and has input state data
        if (targetTank && targetTank.input) {
            // Arras.io tank objects sent over the network usually have an 'input' sub-object 
            // that reflects its current actions. This is the state we need to copy.
            const targetInput = targetTank.input;

            // 1. MIMIC MOVEMENT
            // Copying direct input flags (usually 0 or 1)
            if (targetInput.up !== undefined) myInput.up = targetInput.up;
            if (targetInput.down !== undefined) myInput.down = targetInput.down;
            if (targetInput.left !== undefined) myInput.left = targetInput.left;
            if (targetInput.right !== undefined) myInput.right = targetInput.right;

            // 2. MIMIC SHOOTING
            if (targetInput.shoot !== undefined) {
                myInput.shoot = targetInput.shoot;
            }

            // 3. MIMIC AIMING (Copying the target's aim position)
            if (targetInput.aim_x !== undefined && targetInput.aim_y !== undefined) {
                myInput.aim_x = targetInput.aim_x;
                myInput.aim_y = targetInput.aim_y;
            } 
            
            // Note: Aiming must be copied in world coordinates if the game uses that format.
            // If the game only exposes an aim angle, further conversion logic would be needed.

            console.log(`[Arras Alt Mimic] Mimicking Alt at Index: ${targetTankIndex}`);

        } else {
            // Target tank is gone (maybe disconnected or died)
            console.warn(`[Arras Alt Mimic] Target tank at index ${targetTankIndex} disappeared. Stopping mimic.`);
            isMimicking = false;
            targetTankIndex = -1;
            showNotification("Mimic OFF: Target disconnected/died.");
            // Reset player input to stop unintended movement
            myInput.up = myInput.down = myInput.left = myInput.right = myInput.shoot = 0;
        }
    }

    /**
     * Handles the right-click (contextmenu) event for selecting the alt.
     */
    function handleRightClick(event) {
        // Prevent the default context menu from appearing
        event.preventDefault(); 
        
        if (!isInitialized) {
            showNotification("Initialization not complete. Try reloading the page.");
            return;
        }

        const canvas = gameVars.canvas;

        // Get mouse coordinates relative to the canvas
        const rect = canvas.getBoundingClientRect();
        const mouseX = event.clientX - rect.left;
        const mouseY = event.clientY - rect.top;

        // Convert to world coordinates
        const worldCoords = screenToWorldCoords(mouseX, mouseY);
        
        const newTargetIndex = findTargetTank(worldCoords.wx, worldCoords.wy);

        if (isMimicking && newTargetIndex === targetTankIndex) {
            // Case 1: Already mimicking this target -> STOP MIMICKING
            isMimicking = false;
            targetTankIndex = -1;
            console.log("[Arras Alt Mimic] Stopped mimicking.");
            showNotification("Alt Mimic: OFF");
            // Reset player input
            const myInput = gameVars.myInputObject;
            if(myInput) myInput.up = myInput.down = myInput.left = myInput.right = myInput.shoot = 0;

        } else if (newTargetIndex !== -1) {
            // Case 2: Found a new target -> START MIMICKING
            targetTankIndex = newTargetIndex;
            isMimicking = true;
            console.log(`[Arras Alt Mimic] Started mimicking new Alt at Index: ${targetTankIndex}`);
            showNotification(`Alt Mimic: ON (Following Alt Index: ${targetTankIndex})`);
        } else {
            // Case 3: Right-clicked, but no tank found nearby
            isMimicking = false; // Ensure it's off if no target is found
            targetTankIndex = -1;
            console.log("[Arras Alt Mimic] No tank found near click. Mimic OFF.");
            showNotification("Alt Mimic: No alt found nearby.");
        }
    }

    /**
     * Initialization function
     */
    function init() {
        console.log("[Arras Alt Mimic] Starting Autodetection...");
        
        const detectionSuccess = findGameVariables();

        if (detectionSuccess) {
            isInitialized = true;
            gameVars.canvas.addEventListener('contextmenu', handleRightClick, false);
            // Start the polling loop (runs 60 times per second)
            setInterval(mimicLoop, 1000 / 60); 

            console.log("[Arras Alt Mimic] Userscript loaded and variables autodetected.");
            showNotification("Alt Mimic: Ready. Right-click on an alt to follow.");
        } else {
            console.error("[Arras Alt Mimic] Autodetection failed. Retrying in 2 seconds...");
            // Retry initialization until the global variables are available
            setTimeout(init, 2000); 
        }
    }

    // Start the initialization process
    init();

})();