Right-click on an alt to copy its movement, aiming, and shooting, with automatic variable detection.
当前为
// ==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();
})();