iPhone/iPad touch controls for slither.io with visual pointer and customization
当前为
// ==UserScript==
// @name Slither.io Mobile Mod
// @namespace slither_mobile_mod
// @version 1.0
// @description iPhone/iPad touch controls for slither.io with visual pointer and customization
// @author XBACT
// @match *://slither.com/*
// @match *://slither.io/*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Touch control state
let touchActive = false;
let touchStartX = 0;
let touchStartY = 0;
let touchCurrentX = 0;
let touchCurrentY = 0;
let pointerX = 0;
let pointerY = 0;
let pointerElement = null;
let boostButton = null;
let zoomInButton = null;
let zoomOutButton = null;
let settingsButton = null;
let settingsPanel = null;
let isAccelerating = false;
let lastKnownAngle = 0;
let currentZoom = 1.0;
let gameStartZoom = 1.0;
let editMode = false;
let draggingButton = null;
let dragOffsetX = 0;
let dragOffsetY = 0;
let editingButton = null;
let wasInGame = false;
const POINTER_SPEED_MULTIPLIER = 2.5;
const INITIAL_POINTER_DISTANCE = 120;
const MIN_POINTER_DISTANCE = 10;
const MIN_ZOOM = 0.3;
const MAX_ZOOM = 3.0;
// Default button settings
let buttonSettings = {
boost: {
x: window.innerWidth - 110,
y: window.innerHeight - 110,
width: 80,
height: 80,
color: '#ff3232',
opacity: 0.7,
borderRadius: 50
},
zoomIn: {
x: window.innerWidth - 80,
y: window.innerHeight - 210,
width: 50,
height: 50,
color: '#6496ff',
opacity: 0.7,
borderRadius: 10,
value: 0.05
},
zoomOut: {
x: window.innerWidth - 80,
y: window.innerHeight - 270,
width: 50,
height: 50,
color: '#6496ff',
opacity: 0.7,
borderRadius: 10,
value: 0.05
}
};
// Load saved settings
function loadSettings() {
const saved = localStorage.getItem('slitherMobileSettings');
if (saved) {
try {
const parsed = JSON.parse(saved);
buttonSettings = { ...buttonSettings, ...parsed };
} catch (e) {
console.error('Failed to load settings:', e);
}
}
}
// Save settings
function saveSettings() {
localStorage.setItem('slitherMobileSettings', JSON.stringify(buttonSettings));
}
// Check if currently in game
function isInGame() {
if (window.snake && window.snake.id !== undefined) return true;
if (window.playing === true) return true;
const loginDiv = document.getElementById('login');
if (loginDiv && loginDiv.style.display === 'none') return true;
const playBtn = document.getElementById('playh');
if (playBtn && playBtn.style.display === 'none') return true;
return false;
}
// Monitor game state changes
function checkGameStateChange() {
const currentlyInGame = isInGame();
// Detect game start
if (currentlyInGame && !wasInGame) {
console.log('Game started - resetting zoom to:', gameStartZoom);
currentZoom = gameStartZoom;
if (window.gsc !== undefined) {
window.gsc = currentZoom;
}
}
// Detect game end
if (!currentlyInGame && wasInGame) {
console.log('Game ended - saving zoom:', currentZoom);
gameStartZoom = currentZoom;
}
wasInGame = currentlyInGame;
}
// Run game state check regularly
setInterval(checkGameStateChange, 100);
// Create visual pointer element
function createPointer() {
pointerElement = document.createElement('div');
pointerElement.style.cssText = `
position: fixed;
width: 40px;
height: 40px;
pointer-events: none;
z-index: 10000;
display: none;
transform: translate(-50%, -50%);
`;
pointerElement.innerHTML = `
<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="glow">
<feGaussianBlur stdDeviation="2" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<path d="M20 38 L10 25 L16 25 L16 2 L24 2 L24 25 L30 25 Z"
fill="rgba(100, 200, 255, 0.9)"
stroke="white"
stroke-width="2"
filter="url(#glow)"/>
<circle cx="20" cy="20" r="3" fill="white" opacity="0.8"/>
</svg>
`;
document.body.appendChild(pointerElement);
}
// Apply button style
function applyButtonStyle(button, settings) {
const s = buttonSettings[settings];
button.style.left = s.x + 'px';
button.style.top = s.y + 'px';
button.style.width = s.width + 'px';
button.style.height = s.height + 'px';
button.style.backgroundColor = `${s.color}${Math.round(s.opacity * 255).toString(16).padStart(2, '0')}`;
button.style.borderRadius = s.borderRadius + '%';
}
// Create boost button
function createBoostButton() {
boostButton = document.createElement('button');
boostButton.textContent = 'BOOST';
boostButton.className = 'control-button';
boostButton.dataset.buttonType = 'boost';
boostButton.style.cssText = `
position: fixed;
border: 3px solid white;
color: white;
font-weight: bold;
font-size: 14px;
z-index: 10001;
touch-action: none;
user-select: none;
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
`;
applyButtonStyle(boostButton, 'boost');
boostButton.addEventListener('touchstart', handleBoostStart, { passive: false });
boostButton.addEventListener('touchend', handleBoostEnd, { passive: false });
document.body.appendChild(boostButton);
}
function handleBoostStart(e) {
if (editMode) {
startDragging(e, boostButton);
return;
}
e.preventDefault();
e.stopPropagation();
if (!isInGame()) return;
isAccelerating = true;
boostButton.style.filter = 'brightness(1.3)';
simulateMouseDown();
}
function handleBoostEnd(e) {
if (editMode) return;
e.preventDefault();
e.stopPropagation();
isAccelerating = false;
boostButton.style.filter = 'brightness(1)';
simulateMouseUp();
// Don't stop mouse tracking here - keep it running if touch is still active
}
// Create zoom buttons
function createZoomButtons() {
zoomInButton = document.createElement('button');
zoomInButton.textContent = '+';
zoomInButton.className = 'control-button';
zoomInButton.dataset.buttonType = 'zoomIn';
zoomInButton.style.cssText = `
position: fixed;
border: 2px solid white;
color: white;
font-weight: bold;
font-size: 24px;
z-index: 10001;
touch-action: none;
user-select: none;
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
`;
applyButtonStyle(zoomInButton, 'zoomIn');
zoomInButton.addEventListener('touchstart', (e) => {
if (editMode) {
startDragging(e, zoomInButton);
return;
}
e.preventDefault();
e.stopPropagation();
zoomInButton.style.filter = 'brightness(1.3)';
adjustZoom(buttonSettings.zoomIn.value);
}, { passive: false });
zoomInButton.addEventListener('touchend', (e) => {
if (editMode) return;
e.preventDefault();
e.stopPropagation();
zoomInButton.style.filter = 'brightness(1)';
}, { passive: false });
document.body.appendChild(zoomInButton);
zoomOutButton = document.createElement('button');
zoomOutButton.textContent = '−';
zoomOutButton.className = 'control-button';
zoomOutButton.dataset.buttonType = 'zoomOut';
zoomOutButton.style.cssText = `
position: fixed;
border: 2px solid white;
color: white;
font-weight: bold;
font-size: 24px;
z-index: 10001;
touch-action: none;
user-select: none;
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
`;
applyButtonStyle(zoomOutButton, 'zoomOut');
zoomOutButton.addEventListener('touchstart', (e) => {
if (editMode) {
startDragging(e, zoomOutButton);
return;
}
e.preventDefault();
e.stopPropagation();
zoomOutButton.style.filter = 'brightness(1.3)';
adjustZoom(-buttonSettings.zoomOut.value);
}, { passive: false });
zoomOutButton.addEventListener('touchend', (e) => {
if (editMode) return;
e.preventDefault();
e.stopPropagation();
zoomOutButton.style.filter = 'brightness(1)';
}, { passive: false });
document.body.appendChild(zoomOutButton);
}
// Create settings button
function createSettingsButton() {
settingsButton = document.createElement('button');
settingsButton.innerHTML = '⚙';
settingsButton.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(100, 100, 100, 0.7);
border: 2px solid white;
color: white;
font-size: 20px;
z-index: 10002;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
touch-action: none;
`;
settingsButton.addEventListener('click', (e) => {
e.stopPropagation();
toggleSettingsPanel();
});
settingsButton.addEventListener('touchend', (e) => {
e.preventDefault();
e.stopPropagation();
}, { passive: false });
document.body.appendChild(settingsButton);
}
// Create settings panel
function createSettingsPanel() {
settingsPanel = document.createElement('div');
settingsPanel.id = 'settings-panel';
settingsPanel.style.cssText = `
position: fixed;
top: 60px;
right: 10px;
width: 320px;
max-height: 80vh;
background: rgba(30, 30, 35, 0.98);
border: 2px solid white;
border-radius: 10px;
padding: 15px;
z-index: 10003;
display: none;
overflow-y: auto;
color: white;
font-family: Arial, sans-serif;
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
`;
settingsPanel.innerHTML = `
<h3 style="margin: 0 0 15px 0; text-align: center;">コントロール設定</h3>
<button id="toggleEditMode" style="width: 100%; padding: 10px; margin-bottom: 15px; background: #4CAF50; color: white; border: none; border-radius: 5px; cursor: pointer; font-weight: bold; font-size: 14px;">
移動モード: OFF
</button>
<div id="buttonSettingsContainer"></div>
<button id="resetSettings" style="width: 100%; padding: 10px; margin-top: 15px; background: #f44336; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px;">
設定をリセット
</button>
`;
document.body.appendChild(settingsPanel);
document.getElementById('toggleEditMode').addEventListener('click', toggleEditMode);
document.getElementById('resetSettings').addEventListener('click', resetSettings);
updateSettingsPanel();
}
function toggleSettingsPanel() {
const isVisible = settingsPanel.style.display !== 'none';
settingsPanel.style.display = isVisible ? 'none' : 'block';
if (!isVisible) {
updateSettingsPanel();
}
}
function toggleEditMode() {
editMode = !editMode;
const btn = document.getElementById('toggleEditMode');
btn.textContent = `移動モード: ${editMode ? 'ON' : 'OFF'}`;
btn.style.background = editMode ? '#ff9800' : '#4CAF50';
document.querySelectorAll('.control-button').forEach(button => {
button.style.border = editMode ? '3px dashed yellow' : button.dataset.buttonType === 'boost' ? '3px solid white' : '2px solid white';
});
}
function updateSettingsPanel() {
const container = document.getElementById('buttonSettingsContainer');
container.innerHTML = '';
Object.keys(buttonSettings).forEach(key => {
const s = buttonSettings[key];
const section = document.createElement('div');
section.style.cssText = 'margin-bottom: 15px; padding: 12px; background: rgba(255,255,255,0.1); border-radius: 8px;';
const title = key === 'boost' ? 'ブースト' : key === 'zoomIn' ? 'ズームイン' : 'ズームアウト';
let html = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h4 style="margin: 0;">${title}</h4>
<button class="edit-button" data-button="${key}" style="padding: 5px 12px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">
${editingButton === key ? '閉じる' : '編集'}
</button>
</div>
`;
if (editingButton === key) {
html += `<div style="padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.2);">`;
html += createSlider('幅', `${key}-width`, s.width, 30, 200, 1);
html += createSlider('高さ', `${key}-height`, s.height, 30, 200, 1);
html += createSlider('角丸', `${key}-radius`, s.borderRadius, 0, 50, 1);
html += createSlider('透明度', `${key}-opacity`, s.opacity, 0.1, 1, 0.1);
html += createColorPicker('色', `${key}-color`, s.color);
if (key !== 'boost') {
html += createSlider('変更値', `${key}-value`, s.value, 0.01, 1.0, 0.01);
}
html += `</div>`;
}
section.innerHTML = html;
container.appendChild(section);
// Add event listener for edit button
section.querySelector('.edit-button').addEventListener('click', () => {
editingButton = editingButton === key ? null : key;
updateSettingsPanel();
});
// Add event listeners for controls if this button is being edited
if (editingButton === key) {
document.getElementById(`${key}-width`).addEventListener('input', (e) => updateSetting(key, 'width', parseFloat(e.target.value)));
document.getElementById(`${key}-height`).addEventListener('input', (e) => updateSetting(key, 'height', parseFloat(e.target.value)));
document.getElementById(`${key}-radius`).addEventListener('input', (e) => updateSetting(key, 'borderRadius', parseFloat(e.target.value)));
document.getElementById(`${key}-opacity`).addEventListener('input', (e) => updateSetting(key, 'opacity', parseFloat(e.target.value)));
document.getElementById(`${key}-color`).addEventListener('input', (e) => updateSetting(key, 'color', e.target.value));
if (key !== 'boost') {
document.getElementById(`${key}-value`).addEventListener('input', (e) => updateSetting(key, 'value', parseFloat(e.target.value)));
}
}
});
}
function createSlider(label, id, value, min, max, step) {
return `
<div style="margin: 8px 0;">
<label style="display: block; margin-bottom: 3px; font-size: 12px;">${label}: <span id="${id}-val">${value.toFixed(step < 0.1 ? 2 : 1)}</span></label>
<input type="range" id="${id}" min="${min}" max="${max}" step="${step}" value="${value}" style="width: 100%;">
</div>
`;
}
function createColorPicker(label, id, value) {
return `
<div style="margin: 8px 0;">
<label style="display: block; margin-bottom: 3px; font-size: 12px;">${label}</label>
<input type="color" id="${id}" value="${value}" style="width: 100%; height: 30px; cursor: pointer; border-radius: 4px;">
</div>
`;
}
function updateSetting(buttonType, property, value) {
buttonSettings[buttonType][property] = value;
// Update value display
const valSpan = document.getElementById(`${buttonType}-${property}-val`);
if (valSpan) {
if (property === 'value') {
valSpan.textContent = value.toFixed(2);
} else {
valSpan.textContent = value.toFixed(1);
}
}
// Apply to button
const button = buttonType === 'boost' ? boostButton : buttonType === 'zoomIn' ? zoomInButton : zoomOutButton;
applyButtonStyle(button, buttonType);
saveSettings();
}
function resetSettings() {
if (confirm('設定をリセットしますか?')) {
localStorage.removeItem('slitherMobileSettings');
location.reload();
}
}
function startDragging(e, button) {
e.preventDefault();
e.stopPropagation();
draggingButton = button;
const rect = button.getBoundingClientRect();
dragOffsetX = e.touches[0].clientX - rect.left;
dragOffsetY = e.touches[0].clientY - rect.top;
}
// Adjust zoom level
function adjustZoom(delta) {
currentZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, currentZoom + delta));
gameStartZoom = currentZoom; // Update the base zoom level
if (window.gsc !== undefined) {
window.gsc = currentZoom;
}
console.log('Zoom adjusted to:', currentZoom.toFixed(2));
}
// Get snake angle
function getSnakeAngle() {
if (window.snake) {
if (typeof window.snake.ang !== 'undefined') {
lastKnownAngle = window.snake.ang;
return window.snake.ang;
}
if (typeof window.snake.eang !== 'undefined') {
lastKnownAngle = window.snake.eang;
return window.snake.eang;
}
if (typeof window.snake.wang !== 'undefined') {
lastKnownAngle = window.snake.wang;
return window.snake.wang;
}
}
return lastKnownAngle;
}
// Initialize pointer position
function initializePointerPosition() {
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
const snakeAngle = getSnakeAngle();
pointerX = centerX + Math.cos(snakeAngle) * INITIAL_POINTER_DISTANCE;
pointerY = centerY + Math.sin(snakeAngle) * INITIAL_POINTER_DISTANCE;
}
// Update pointer position
function updatePointerPosition() {
if (!touchActive || !pointerElement || !isInGame()) {
if (pointerElement) pointerElement.style.display = 'none';
return;
}
const fingerDeltaX = touchCurrentX - touchStartX;
const fingerDeltaY = touchCurrentY - touchStartY;
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
const snakeAngle = getSnakeAngle();
const initialX = centerX + Math.cos(snakeAngle) * INITIAL_POINTER_DISTANCE;
const initialY = centerY + Math.sin(snakeAngle) * INITIAL_POINTER_DISTANCE;
let newPointerX = initialX + (fingerDeltaX * POINTER_SPEED_MULTIPLIER);
let newPointerY = initialY + (fingerDeltaY * POINTER_SPEED_MULTIPLIER);
const distanceFromCenter = Math.sqrt(Math.pow(newPointerX - centerX, 2) + Math.pow(newPointerY - centerY, 2));
if (distanceFromCenter < MIN_POINTER_DISTANCE) {
const angle = Math.atan2(newPointerY - centerY, newPointerX - centerX);
newPointerX = centerX + Math.cos(angle) * MIN_POINTER_DISTANCE;
newPointerY = centerY + Math.sin(angle) * MIN_POINTER_DISTANCE;
}
pointerX = newPointerX;
pointerY = newPointerY;
pointerElement.style.left = pointerX + 'px';
pointerElement.style.top = pointerY + 'px';
pointerElement.style.display = 'block';
const angleToCenter = Math.atan2(centerY - pointerY, centerX - pointerX);
const rotationDegrees = (angleToCenter * 180 / Math.PI) + 90;
pointerElement.style.transform = `translate(-50%, -50%) rotate(${rotationDegrees}deg)`;
}
// Simulate mouse events
let mouseUpdateInterval = null;
function startMouseTracking() {
if (mouseUpdateInterval) return;
mouseUpdateInterval = setInterval(() => {
if (touchActive && isInGame()) {
simulateMouseMove(pointerX, pointerY);
}
}, 16);
}
function stopMouseTracking() {
if (mouseUpdateInterval) {
clearInterval(mouseUpdateInterval);
mouseUpdateInterval = null;
}
}
function simulateMouseMove(x, y) {
if (!isInGame()) return;
const canvas = document.querySelector('canvas');
if (!canvas) return;
const event = new MouseEvent('mousemove', {
clientX: x,
clientY: y,
bubbles: true,
cancelable: true,
view: window
});
canvas.dispatchEvent(event);
}
function simulateMouseDown() {
if (!isInGame()) return;
const canvas = document.querySelector('canvas');
if (!canvas) return;
const event = new MouseEvent('mousedown', {
button: 0,
buttons: 1,
bubbles: true,
cancelable: true,
view: window
});
canvas.dispatchEvent(event);
}
function simulateMouseUp() {
const canvas = document.querySelector('canvas');
if (!canvas) return;
const event = new MouseEvent('mouseup', {
button: 0,
buttons: 0,
bubbles: true,
cancelable: true,
view: window
});
canvas.dispatchEvent(event);
}
// Touch event handlers
document.addEventListener('touchstart', (e) => {
// Allow settings panel interaction
if (e.target.closest('#settings-panel')) {
return;
}
// Allow settings button
if (e.target === settingsButton) {
return;
}
// Allow control buttons
if (e.target.closest('.control-button')) {
return;
}
// For menu screen, only prevent default but don't start pointer tracking
if (!isInGame()) {
// Allow normal touch interaction on menu
return;
}
// In game: block and handle with pointer
e.preventDefault();
e.stopPropagation();
touchActive = true;
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
touchCurrentX = touchStartX;
touchCurrentY = touchStartY;
initializePointerPosition();
updatePointerPosition();
startMouseTracking();
}, { passive: false, capture: true });
document.addEventListener('touchmove', (e) => {
if (draggingButton) {
e.preventDefault();
e.stopPropagation();
const newX = e.touches[0].clientX - dragOffsetX;
const newY = e.touches[0].clientY - dragOffsetY;
const buttonType = draggingButton.dataset.buttonType;
buttonSettings[buttonType].x = newX;
buttonSettings[buttonType].y = newY;
draggingButton.style.left = newX + 'px';
draggingButton.style.top = newY + 'px';
saveSettings();
return;
}
if (!touchActive) return;
// Block native touch events only in game
if (isInGame() && !e.target.closest('#settings-panel')) {
e.preventDefault();
e.stopPropagation();
}
touchCurrentX = e.touches[0].clientX;
touchCurrentY = e.touches[0].clientY;
if (isInGame()) {
updatePointerPosition();
}
}, { passive: false, capture: true });
document.addEventListener('touchend', (e) => {
if (draggingButton) {
draggingButton = null;
e.preventDefault();
e.stopPropagation();
return;
}
if (!e.target.closest('.control-button') && e.target !== settingsButton && !e.target.closest('#settings-panel')) {
if (isInGame()) {
e.preventDefault();
e.stopPropagation();
}
}
touchActive = false;
stopMouseTracking();
if (pointerElement) {
pointerElement.style.display = 'none';
}
}, { passive: false, capture: true });
// Initialize
function init() {
loadSettings();
createPointer();
createBoostButton();
createZoomButtons();
createSettingsButton();
createSettingsPanel();
const canvas = document.querySelector('canvas');
if (canvas) {
canvas.style.touchAction = 'none';
}
if (window.gsc !== undefined) {
currentZoom = window.gsc;
gameStartZoom = currentZoom;
}
wasInGame = isInGame();
console.log('Slither.io mobile controls initialized');
console.log('Initial zoom:', currentZoom.toFixed(2));
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();