// ==UserScript== // @name Torn Christmas Town Helper // @namespace http://tampermonkey.net/ // @version 1.7 // @description Auto-run movement and present highlighting for Christmas Town // @author Getty111 [3955428] // @contributor Claude did most of the work. // @homepageURL https://www.torn.com/profiles.php?XID=3955428 // @match https://www.torn.com/christmas_town.php* // @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com // @grant none // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; let isAutoRunning = false; let autoRunInterval = null; let currentDirection = null; let statusDiv = null; let audioContext = null; let arrowDiv = null; let activeItems = new Map(); // Track items and their positions // Movement speed in ms (lower = faster, but don't go too fast or server might reject) const MOVE_INTERVAL = 300; // Direction mappings - class names on the li elements const DIRECTIONS = { 'top': { angle: -90, range: 22.5 }, 'right-top': { angle: -45, range: 22.5 }, 'right': { angle: 0, range: 22.5 }, 'right-bottom': { angle: 45, range: 22.5 }, 'bottom': { angle: 90, range: 22.5 }, 'left-bottom': { angle: 135, range: 22.5 }, 'left': { angle: 180, range: 22.5 }, 'left-top': { angle: -135, range: 22.5 } }; function init() { console.log('[CT Helper] Initializing Christmas Town Helper...'); // Load saved settings loadSettings(); // Wait for the map to load waitForElement('#user-map', (mapElement) => { console.log('[CT Helper] Map found, setting up...'); createStatusDisplay(); applySettingsToUI(); setupMapClickHandler(mapElement); setupPresentHighlighting(); setupKeyboardShortcuts(); initAudio(); watchInventory(); watchStatusMessages(); }); } function applySettingsToUI() { // Apply sound setting const hotkeyM = document.getElementById('ct-hotkey-m'); if (hotkeyM) { hotkeyM.style.color = soundEnabled ? '#00ff00' : '#ff4444'; } const volumeSlider = document.getElementById('ct-volume-slider'); if (volumeSlider) { volumeSlider.value = soundVolume * 100; } // Apply arrow setting const hotkeyP = document.getElementById('ct-hotkey-p'); if (hotkeyP) { hotkeyP.style.color = arrowEnabled ? '#00ff00' : '#ff4444'; } // Apply highlights setting if (highlightsEnabled) { document.body.classList.add('ct-helper-highlight-items'); } else { document.body.classList.remove('ct-helper-highlight-items'); } const hotkeyH = document.getElementById('ct-hotkey-h'); if (hotkeyH) { hotkeyH.style.color = highlightsEnabled ? '#00ff00' : '#ff4444'; } } function waitForElement(selector, callback, maxAttempts = 50) { let attempts = 0; const check = () => { const element = document.querySelector(selector); if (element) { callback(element); } else if (attempts < maxAttempts) { attempts++; setTimeout(check, 200); } else { console.log('[CT Helper] Element not found:', selector); } }; check(); } function createStatusDisplay() { statusDiv = document.createElement('div'); statusDiv.id = 'ct-helper-status'; statusDiv.style.cssText = ` position: fixed; top: 10px; right: 10px; background: rgba(0, 0, 0, 0.8); color: #00ff00; padding: 10px 15px; border-radius: 5px; font-family: monospace; font-size: 12px; z-index: 99999; border: 1px solid #00ff00; min-width: 150px; `; statusDiv.innerHTML = `
🎄 CT Helper
Status: Idle
Direction: -
Click map to auto-run
Click again to stop
[H] Highlights
[M] Sound
[P] Arrow
[Space] Stop
`; document.body.appendChild(statusDiv); // Create arrow indicator createArrowIndicator(); // Setup volume slider const volumeSlider = document.getElementById('ct-volume-slider'); if (volumeSlider) { volumeSlider.addEventListener('input', (e) => { soundVolume = e.target.value / 100; console.log('[CT Helper] Volume:', Math.round(soundVolume * 100) + '%'); }); // Test sound on change and save volumeSlider.addEventListener('change', () => { saveSettings(); if (soundEnabled) { playBing(); } }); } } function createArrowIndicator() { arrowDiv = document.createElement('div'); arrowDiv.id = 'ct-helper-arrow'; arrowDiv.style.cssText = ` position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 40px; height: 40px; pointer-events: none; z-index: 999; display: none; `; arrowDiv.innerHTML = `
`; // Add to map container instead of body waitForElement('.user-map-container', (mapContainer) => { mapContainer.style.position = 'relative'; mapContainer.appendChild(arrowDiv); console.log('[CT Helper] Arrow added to map container'); }); } function updateStatus(status, direction) { const statusText = document.getElementById('ct-status-text'); const directionText = document.getElementById('ct-direction-text'); if (statusText) { statusText.textContent = status; statusText.style.color = status === 'Running' ? '#00ff00' : '#ff6600'; } if (directionText) { directionText.textContent = direction || '-'; } } function setupMapClickHandler(mapElement) { // Create an invisible overlay for click detection const overlay = document.createElement('div'); overlay.id = 'ct-click-overlay'; overlay.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000; cursor: crosshair; pointer-events: auto; `; // Find the map container const mapContainer = mapElement.closest('.user-map-container') || mapElement; mapContainer.style.position = 'relative'; // Add click handler overlay.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); if (isAutoRunning) { stopAutoRun(); } else { const rect = overlay.getBoundingClientRect(); const centerX = rect.width / 2; const centerY = rect.height / 2; const clickX = e.clientX - rect.left; const clickY = e.clientY - rect.top; // Calculate angle from center const angle = Math.atan2(clickY - centerY, clickX - centerX) * (180 / Math.PI); const direction = getDirectionFromAngle(angle); if (direction) { startAutoRun(direction); } } }); // Right-click to stop overlay.addEventListener('contextmenu', (e) => { e.preventDefault(); if (isAutoRunning) { stopAutoRun(); } }); mapContainer.appendChild(overlay); console.log('[CT Helper] Click overlay added'); } function getDirectionFromAngle(angle) { // Normalize angle to -180 to 180 while (angle > 180) angle -= 360; while (angle < -180) angle += 360; // Map angles to directions (0 = right, -90 = up, 90 = down, 180/-180 = left) if (angle >= -22.5 && angle < 22.5) return 'right'; if (angle >= 22.5 && angle < 67.5) return 'right-bottom'; if (angle >= 67.5 && angle < 112.5) return 'bottom'; if (angle >= 112.5 && angle < 157.5) return 'left-bottom'; if (angle >= 157.5 || angle < -157.5) return 'left'; if (angle >= -157.5 && angle < -112.5) return 'left-top'; if (angle >= -112.5 && angle < -67.5) return 'top'; if (angle >= -67.5 && angle < -22.5) return 'right-top'; return null; } function startAutoRun(direction) { currentDirection = direction; isAutoRunning = true; console.log('[CT Helper] Starting auto-run:', direction); updateStatus('Running', direction); // Immediately move once triggerMove(direction); // Then set up interval autoRunInterval = setInterval(() => { triggerMove(direction); }, MOVE_INTERVAL); } function stopAutoRun() { isAutoRunning = false; currentDirection = null; if (autoRunInterval) { clearInterval(autoRunInterval); autoRunInterval = null; } console.log('[CT Helper] Stopped auto-run'); updateStatus('Idle', null); } function triggerMove(direction) { // Find the direction control element const controlSelector = `ul.map-controls li.${direction}`; const control = document.querySelector(controlSelector); if (control) { // Simulate mousedown and mouseup (how the game handles movement) const mousedownEvent = new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }); const mouseupEvent = new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }); const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: window }); control.dispatchEvent(mousedownEvent); control.dispatchEvent(clickEvent); control.dispatchEvent(mouseupEvent); } else { console.log('[CT Helper] Direction control not found:', direction); } } function setupPresentHighlighting() { // Add CSS for highlighting presents/items const style = document.createElement('style'); style.id = 'ct-helper-styles'; style.textContent = ` /* Only highlight actual collectible items in the items-layer on the map */ /* The items-layer is specifically where pickups spawn, not decorations */ .ct-helper-highlight-items #world .items-layer > * { filter: drop-shadow(0 0 8px #00ff00) drop-shadow(0 0 16px #ffff00) !important; animation: ct-glow 0.5s ease-in-out infinite alternate !important; } @keyframes ct-glow { from { filter: drop-shadow(0 0 8px #00ff00) drop-shadow(0 0 12px #ffff00); } to { filter: drop-shadow(0 0 12px #00ff00) drop-shadow(0 0 20px #ff6600); } } /* Make the overlay visible when auto-running */ #ct-click-overlay.running { background: radial-gradient(circle, transparent 40%, rgba(0, 255, 0, 0.1) 100%); } `; document.head.appendChild(style); // Highlighting state is now applied by applySettingsToUI() // Also set up a MutationObserver to watch for new items appearing observeForItems(); } function observeForItems() { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === 1) { // Element node // Check if it's an item or contains items const items = node.querySelectorAll ? node.querySelectorAll('.items-layer *, [class*="item"], [src*="item"]') : []; if (items.length > 0) { console.log('[CT Helper] New items detected on map'); } } }); }); }); const world = document.getElementById('world'); if (world) { observer.observe(world, { childList: true, subtree: true }); } } function setupKeyboardShortcuts() { document.addEventListener('keydown', (e) => { // Only handle if not typing in an input if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; switch(e.key.toLowerCase()) { case ' ': // Space to stop case 'escape': if (isAutoRunning) { e.preventDefault(); stopAutoRun(); } break; case 'h': // Toggle highlighting highlightsEnabled = !highlightsEnabled; document.body.classList.toggle('ct-helper-highlight-items', highlightsEnabled); console.log('[CT Helper] Highlighting:', highlightsEnabled ? 'ON' : 'OFF'); const hotkeyH = document.getElementById('ct-hotkey-h'); if (hotkeyH) { hotkeyH.style.color = highlightsEnabled ? '#00ff00' : '#ff4444'; } saveSettings(); break; case 'm': // Toggle sound toggleSound(); break; case 'p': // Toggle arrow (P for pointer) toggleArrow(); break; } }); // Stop auto-run when tab loses visibility (switching tabs) document.addEventListener('visibilitychange', () => { if (document.hidden && isAutoRunning) { console.log('[CT Helper] Tab hidden, stopping auto-run'); stopAutoRun(); } }); } function initAudio() { // Create audio context on first user interaction document.addEventListener('click', () => { if (!audioContext) { audioContext = new (window.AudioContext || window.webkitAudioContext)(); console.log('[CT Helper] Audio initialized'); } }, { once: true }); } let soundEnabled = false; let soundVolume = 0.3; // 0 to 1 let arrowEnabled = true; let highlightsEnabled = true; const STORAGE_KEY = 'ct-helper-settings'; function loadSettings() { try { const saved = localStorage.getItem(STORAGE_KEY); if (saved) { const settings = JSON.parse(saved); soundEnabled = settings.soundEnabled ?? false; soundVolume = settings.soundVolume ?? 0.3; arrowEnabled = settings.arrowEnabled ?? true; highlightsEnabled = settings.highlightsEnabled ?? true; console.log('[CT Helper] Settings loaded:', settings); } } catch (e) { console.log('[CT Helper] Could not load settings:', e); } } function saveSettings() { try { const settings = { soundEnabled, soundVolume, arrowEnabled, highlightsEnabled }; localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); console.log('[CT Helper] Settings saved'); } catch (e) { console.log('[CT Helper] Could not save settings:', e); } } function toggleArrow() { arrowEnabled = !arrowEnabled; console.log('[CT Helper] Arrow:', arrowEnabled ? 'ON' : 'OFF'); if (!arrowEnabled && arrowDiv) { arrowDiv.style.display = 'none'; } const hotkeyEl = document.getElementById('ct-hotkey-p'); if (hotkeyEl) { hotkeyEl.style.color = arrowEnabled ? '#00ff00' : '#ff4444'; } saveSettings(); } function toggleSound() { soundEnabled = !soundEnabled; console.log('[CT Helper] Sound:', soundEnabled ? 'ON' : 'OFF'); const hotkeyEl = document.getElementById('ct-hotkey-m'); if (hotkeyEl) { hotkeyEl.style.color = soundEnabled ? '#00ff00' : '#ff4444'; } saveSettings(); } function playBing() { if (!soundEnabled) return; if (!audioContext) { audioContext = new (window.AudioContext || window.webkitAudioContext)(); } try { // Resume context if suspended (browser autoplay policy) if (audioContext.state === 'suspended') { audioContext.resume(); } // Create a pleasant "bing" sound const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); // Nice chime frequency oscillator.frequency.setValueAtTime(880, audioContext.currentTime); // A5 oscillator.frequency.setValueAtTime(1108.73, audioContext.currentTime + 0.1); // C#6 oscillator.type = 'sine'; // Quick fade in and out gainNode.gain.setValueAtTime(0, audioContext.currentTime); gainNode.gain.linearRampToValueAtTime(soundVolume, audioContext.currentTime + 0.05); gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 0.4); oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.4); console.log('[CT Helper] 🔔 BING! Item collected!'); } catch (e) { console.log('[CT Helper] Audio error:', e); } } function watchInventory() { // Watch the items-layer on the map for new collectibles appearing waitForElement('#world .items-layer', (itemsLayer) => { console.log('[CT Helper] Items layer found, watching for collectibles...'); // Track items we've already binged for (by their style/position) const seenItems = new Set(); // Check for any existing items on load const existingItems = itemsLayer.querySelectorAll('.ct-item, [class*="item"], div'); existingItems.forEach(item => { const itemKey = getItemKey(item); if (itemKey) { seenItems.add(itemKey); trackItem(item, itemKey); } }); const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { // Handle added nodes mutation.addedNodes.forEach((node) => { if (node.nodeType === 1) { // Element node const itemKey = getItemKey(node); if (itemKey && !seenItems.has(itemKey)) { seenItems.add(itemKey); console.log('[CT Helper] 🎁 New item appeared on map!', itemKey); playBing(); trackItem(node, itemKey); } } }); // Handle removed nodes (item picked up) mutation.removedNodes.forEach((node) => { if (node.nodeType === 1) { const itemKey = getItemKey(node); if (itemKey) { activeItems.delete(itemKey); updateArrow(); } } }); }); }); observer.observe(itemsLayer, { childList: true, subtree: true }); console.log('[CT Helper] Item appearance watcher active'); // Start arrow update loop setInterval(updateArrow, 500); }); } function trackItem(element, itemKey) { const pos = getItemPosition(element); if (pos) { activeItems.set(itemKey, { element, pos }); updateArrow(); } } function getItemPosition(element) { if (!element || !element.style) return null; const style = element.getAttribute('style') || ''; const leftMatch = style.match(/left:\s*(-?\d+)px/); const topMatch = style.match(/top:\s*(-?\d+)px/); if (leftMatch && topMatch) { return { x: parseInt(leftMatch[1]), y: parseInt(topMatch[1]) }; } return null; } function getPlayerPosition() { // The 'you' class is on an inner element, so find its parent .ct-user const youMarker = document.querySelector('.img-wrap.you, .svgImageWrap.you'); const player = youMarker ? youMarker.closest('.ct-user') : null; if (!player) return null; const style = player.getAttribute('style') || ''; const transformMatch = style.match(/translate\((-?\d+)px,\s*(-?\d+)px\)/); if (transformMatch) { return { x: parseInt(transformMatch[1]), y: parseInt(transformMatch[2]) }; } return null; } function updateArrow() { if (!arrowDiv || !arrowEnabled) { if (arrowDiv) arrowDiv.style.display = 'none'; return; } const playerPos = getPlayerPosition(); if (!playerPos || activeItems.size === 0) { arrowDiv.style.display = 'none'; return; } // Find the closest item let closestItem = null; let closestDist = Infinity; activeItems.forEach((item, key) => { // Check if element still exists in DOM if (!document.contains(item.element)) { activeItems.delete(key); return; } const dx = item.pos.x - playerPos.x; const dy = item.pos.y - playerPos.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < closestDist) { closestDist = dist; closestItem = item; } }); if (!closestItem) { arrowDiv.style.display = 'none'; return; } // Calculate angle to item const dx = closestItem.pos.x - playerPos.x; const dy = closestItem.pos.y - playerPos.y; const angle = Math.atan2(dy, dx) * (180 / Math.PI) + 90; // +90 because arrow points up by default // Show arrow and rotate it arrowDiv.style.display = 'block'; arrowDiv.querySelector('svg').style.transform = `rotate(${angle}deg)`; // Show distance (in tiles, roughly 30px per tile) const distInTiles = Math.round(closestDist / 30); const distanceDiv = document.getElementById('ct-arrow-distance'); if (distanceDiv) { distanceDiv.textContent = `~${distInTiles} tiles`; } } function getItemKey(element) { // Create a unique key for an item based on its position/style if (!element || !element.style) return null; const style = element.getAttribute('style') || ''; const className = element.className || ''; // Use position as unique identifier if (style.includes('left:') && style.includes('top:')) { return `${className}-${style}`; } return null; } // Also watch for status messages about finding items function watchStatusMessages() { // Keep this as a backup detection method const statusContainer = document.querySelector('.status-area-container, .text-container'); if (!statusContainer) { setTimeout(watchStatusMessages, 1000); return; } let lastMessage = ''; const observer = new MutationObserver((mutations) => { const text = statusContainer.textContent.toLowerCase(); // Avoid duplicate bings for the same message if (text !== lastMessage) { lastMessage = text; // Only bing on actual pickup messages, not appearance // (we handle appearance separately now) } }); observer.observe(statusContainer, { childList: true, subtree: true, characterData: true }); console.log('[CT Helper] Status message watcher active'); } // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();