Torn Pickpocket Module

Enhanced pickpocketing with color coding and auto-picker - Framework Module

目前為 2025-09-24 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Torn Pickpocket Module
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Enhanced pickpocketing with color coding and auto-picker - Framework Module
// @match        https://www.torn.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        unsafeWindow
// @license MIT (With Credit)
// ==/UserScript==

(function() {
    'use strict';

    // Use unsafeWindow to match framework
    const globalWindow = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;

    console.log('[PICKPOCKET] Module starting...');

    // PICKPOCKETING CONFIG
    const categoryColorMap = {
        "Safe": "#37b24d",
        "Moderately Unsafe": "#74b816",
        "Unsafe": "#f59f00",
        "Risky": "#f76707",
        "Dangerous": "#f03e3e",
        "Very Dangerous": "#7048e8",
    };

    const markGroups = {
        "Safe": ["Drunk man", "Drunk woman", "Homeless person", "Junkie", "Elderly man", "Elderly woman"],
        "Moderately Unsafe": ["Classy lady", "Laborer", "Postal worker", "Young man", "Young woman", "Student"],
        "Unsafe": ["Rich kid", "Sex worker", "Thug"],
        "Risky": ["Jogger", "Businessman", "Businesswoman", "Gang member", "Mobster"],
        "Dangerous": ["Cyclist"],
        "Very Dangerous": ["Police officer"],
    };

    const activityTypes = ["Walking", "Stumbling", "Loitering", "Listening to music", "Distracted", "Soliciting", "Running", "Jogging"];

    // Settings
    let ppSettings = {
        coloringEnabled: GM_getValue('ppColoringEnabled', true),
        autoPickerEnabled: GM_getValue('ppAutoPickerEnabled', false),
        targetCategories: GM_getValue('ppTargetCategories', ["Safe"]),
        targetActivities: GM_getValue('ppTargetActivities', ["Stumbling"]),
        autoPickDelay: GM_getValue('ppAutoPickDelay', 3000),
        maxNerve: GM_getValue('ppMaxNerve', 5),
        randomDelay: GM_getValue('ppRandomDelay', true),
        maxActions: GM_getValue('ppMaxActions', 50),
        safetyMode: GM_getValue('ppSafetyMode', true)
    };

    // State
    let processedTargets = new Set();
    let autoPickerInterval = null;
    let actionCount = 0;
    let sessionStart = Date.now();

    function saveSettings() {
        Object.keys(ppSettings).forEach(key => {
            GM_setValue(`pp${key.charAt(0).toUpperCase() + key.slice(1)}`, ppSettings[key]);
        });
    }

    function log(msg, type = 'info') {
        if (globalWindow.TornFramework && globalWindow.TornFramework.log) {
            globalWindow.TornFramework.log(msg, type, 'PICKPOCKET');
        } else {
            console.log(`[PICKPOCKET] ${msg}`);
        }
    }

    function addRandomDelay(baseDelay) {
        if (!ppSettings.randomDelay) return baseDelay;
        const variance = baseDelay * 0.3;
        return baseDelay + (Math.random() * variance * 2 - variance);
    }

    function isRateLimited() {
        if (!ppSettings.safetyMode) return false;
        if (actionCount >= ppSettings.maxActions) {
            return true;
        }
        return false;
    }

    function processCurrentTargets() {
        if (!ppSettings.coloringEnabled) return;

        try {
            const targetElements = document.querySelectorAll('.titleAndProps___DdeVu > div');

            targetElements.forEach((titleDiv) => {
                const titleText = titleDiv.textContent.trim().split(' (')[0];
                if (!titleText) return;

                let category = null;
                for (const cat in markGroups) {
                    if (markGroups[cat].includes(titleText)) {
                        category = cat;
                        break;
                    }
                }

                if (category) {
                    titleDiv.style.color = categoryColorMap[category];
                    titleDiv.style.fontWeight = 'bold';
                    titleDiv.style.textShadow = '0 1px 2px rgba(0,0,0,0.5)';

                    if (!titleDiv.textContent.includes(`(${category})`)) {
                        titleDiv.textContent = `${titleText} (${category})`;
                    }
                }
            });
        } catch (error) {
            log(`Target processing failed: ${error.message}`, 'error');
        }
    }

    function getCurrentNerve() {
        try {
            const nerveElement = document.querySelector('.bar-value___NTdce');
            if (nerveElement) {
                const nerveText = nerveElement.textContent;
                const match = nerveText.match(/(\d+)/);
                return match ? parseInt(match[1]) : 0;
            }

            const altNerveElement = document.querySelector('[class*="nerve"] .bar-value, .nerve-bar .bar-value');
            if (altNerveElement) {
                const match = altNerveElement.textContent.match(/(\d+)/);
                return match ? parseInt(match[1]) : 0;
            }

            return 100;
        } catch (error) {
            log(`Nerve reading failed: ${error.message}`, 'error');
            return 0;
        }
    }

    function getCategoryPriority(category) {
        const priorities = {
            "Safe": 1,
            "Moderately Unsafe": 2,
            "Unsafe": 3,
            "Risky": 4,
            "Dangerous": 5,
            "Very Dangerous": 6
        };
        return priorities[category] || 999;
    }

    function findBestTarget() {
        if (isRateLimited()) {
            log('Rate limited - skipping target search', 'warning');
            return null;
        }

        try {
            const crimeOptions = document.querySelectorAll('.crime-option');
            const currentNerve = getCurrentNerve();

            if (currentNerve < 1) {
                log('Insufficient nerve for pickpocketing', 'warning');
                return null;
            }

            const validTargets = [];

            for (const option of crimeOptions) {
                const titleElement = option.querySelector('.titleAndProps___DdeVu > div');
                const activityElement = option.querySelector('.activity___e7mdA');
                const buttonElement = option.querySelector('.commit-button');

                if (!titleElement || !buttonElement) continue;

                const titleText = titleElement.textContent.trim().split(' (')[0];
                const activityText = activityElement ? activityElement.textContent.trim().split('\n')[0] : '';

                const ariaLabel = buttonElement.getAttribute('aria-label') || '';
                const nerveMatch = ariaLabel.match(/(\d+)\s*nerve/i);
                const nerveCost = nerveMatch ? parseInt(nerveMatch[1]) : 5;

                if (currentNerve < nerveCost || nerveCost > ppSettings.maxNerve) continue;
                if (buttonElement.getAttribute('aria-disabled') === 'true') continue;

                let targetCategory = null;
                for (const cat in markGroups) {
                    if (markGroups[cat].includes(titleText)) {
                        targetCategory = cat;
                        break;
                    }
                }

                if (!targetCategory || !ppSettings.targetCategories.includes(targetCategory)) continue;

                if (ppSettings.targetActivities.length > 0) {
                    if (!ppSettings.targetActivities.some(activity => activityText.includes(activity))) continue;
                }

                validTargets.push({
                    element: buttonElement,
                    title: titleText,
                    activity: activityText,
                    category: targetCategory,
                    nerve: nerveCost,
                    priority: getCategoryPriority(targetCategory)
                });
            }

            validTargets.sort((a, b) => {
                if (a.priority !== b.priority) return a.priority - b.priority;
                return a.nerve - b.nerve;
            });

            return validTargets[0] || null;

        } catch (error) {
            log(`Target search failed: ${error.message}`, 'error');
            return null;
        }
    }

    function performAutoPick(isTest = false) {
        try {
            const target = findBestTarget();

            if (!target) {
                const msg = isTest ? 'No valid targets available for test' : 'No targets match current criteria';
                log(msg, 'warning');
                return false;
            }

            if (isTest) {
                log(`Test successful: Would pick ${target.title} (${target.category}) - ${target.activity} [${target.nerve} nerve]`, 'success');
                return true;
            }

            if (ppSettings.safetyMode && isRateLimited()) {
                log('Safety mode: Action blocked due to rate limiting', 'warning');
                return false;
            }

            target.element.click();
            actionCount++;

            log(`Auto-picked: ${target.title} (${target.category}) [${target.nerve} nerve] - Activity: ${target.activity}`, 'success');

            processedTargets.add(target.title + target.activity);
            setTimeout(() => {
                processedTargets.delete(target.title + target.activity);
            }, 30000);

            return true;

        } catch (error) {
            log(`Auto pick failed: ${error.message}`, 'error');
            return false;
        }
    }

    function startAutoPicker() {
        if (autoPickerInterval) return;

        try {
            log('Auto picker starting...', 'success');

            autoPickerInterval = setInterval(() => {
                if (ppSettings.autoPickerEnabled && isOnPickpocketPage()) {
                    if (ppSettings.safetyMode && isRateLimited()) {
                        log('Auto picker paused - rate limit reached', 'warning');
                        return;
                    }
                    performAutoPick();
                }
            }, addRandomDelay(ppSettings.autoPickDelay));

        } catch (error) {
            log(`Auto picker start failed: ${error.message}`, 'error');
        }
    }

    function stopAutoPicker() {
        if (autoPickerInterval) {
            clearInterval(autoPickerInterval);
            autoPickerInterval = null;
            log('Auto picker stopped', 'warning');
        }
    }

    function isOnPickpocketPage() {
        return window.location.href.includes('crimes') &&
               (document.querySelector('.pickpocketing-root') !== null ||
                document.querySelector('[class*="pickpocket"]') !== null ||
                document.querySelector('.crime-option') !== null);
    }

    // Create menu section HTML
    const menuSection = `
        <h4 style="margin: 0 0 12px 0; color: #37b24d;">👤 Pickpocketing</h4>

        <div style="display: grid; grid-template-columns: 1fr auto; gap: 10px; align-items: center; margin-bottom: 12px;">
            <label style="display: flex; align-items: center; cursor: pointer;">
                <input type="checkbox" id="ppColoringEnabled" ${ppSettings.coloringEnabled ? 'checked' : ''}>
                <span style="margin-left: 8px;">Target Color Coding</span>
            </label>
            <div style="font-size: 10px; color: #999;">Visual aid</div>
        </div>

        <div style="display: flex; align-items: center; margin-bottom: 12px; padding: 8px; background: rgba(55,178,77,0.1); border-radius: 6px;">
            <label style="display: flex; align-items: center; cursor: pointer; flex-grow: 1;">
                <input type="checkbox" id="ppAutoPickerEnabled" ${ppSettings.autoPickerEnabled ? 'checked' : ''}>
                <span style="margin-left: 8px; font-weight: bold; color: #37b24d;">🤖 Auto Picker</span>
            </label>
            <div id="ppAutoPickStatus" style="padding: 4px 8px; border-radius: 4px; font-size: 10px; font-weight: bold; background: ${ppSettings.autoPickerEnabled ? '#37b24d' : '#666'}; color: white;">
                ${ppSettings.autoPickerEnabled ? 'ACTIVE' : 'DISABLED'}
            </div>
        </div>

        <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px;">
            <div>
                <label style="display: block; margin-bottom: 4px; font-size: 10px;">Base Delay (ms):</label>
                <input type="number" id="ppAutoPickDelay" value="${ppSettings.autoPickDelay}" min="1000" max="30000" step="500" style="width: 100%; padding: 6px; background: #333; color: white; border: 1px solid #555; border-radius: 4px; font-size: 11px;">
            </div>
            <div>
                <label style="display: block; margin-bottom: 4px; font-size: 10px;">Max Nerve Cost:</label>
                <input type="number" id="ppMaxNerve" value="${ppSettings.maxNerve}" min="1" max="50" style="width: 100%; padding: 6px; background: #333; color: white; border: 1px solid #555; border-radius: 4px; font-size: 11px;">
            </div>
        </div>

        <div style="margin-bottom: 12px;">
            <label style="display: block; margin-bottom: 6px; font-weight: bold; font-size: 11px;">🎯 Target Categories:</label>
            <div id="ppCategoryCheckboxes" style="max-height: 120px; overflow-y: auto; border: 1px solid #555; padding: 8px; border-radius: 4px; background: rgba(0,0,0,0.3);">
                ${Object.keys(markGroups).map(category => `
                    <label style="display: block; margin-bottom: 4px; cursor: pointer; font-size: 11px; padding: 2px 4px; border-radius: 3px; transition: background 0.2s;">
                        <input type="checkbox" class="ppCategoryCheck" value="${category}" ${ppSettings.targetCategories.includes(category) ? 'checked' : ''}>
                        <span style="margin-left: 6px; color: ${categoryColorMap[category]}; font-weight: bold;">●</span>
                        <span style="margin-left: 4px;">${category}</span>
                    </label>
                `).join('')}
            </div>
        </div>

        <div style="margin-bottom: 12px;">
            <label style="display: block; margin-bottom: 6px; font-weight: bold; font-size: 11px;">🚶 Target Activities:</label>
            <div id="ppActivityCheckboxes" style="max-height: 100px; overflow-y: auto; border: 1px solid #555; padding: 8px; border-radius: 4px; background: rgba(0,0,0,0.3);">
                ${activityTypes.map(activity => `
                    <label style="display: block; margin-bottom: 3px; cursor: pointer; font-size: 10px; padding: 2px 4px; border-radius: 3px; transition: background 0.2s;">
                        <input type="checkbox" class="ppActivityCheck" value="${activity}" ${ppSettings.targetActivities.includes(activity) ? 'checked' : ''}>
                        <span style="margin-left: 6px;">${activity}</span>
                    </label>
                `).join('')}
            </div>
        </div>

        <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px;">
            <div>
                <label style="display: block; margin-bottom: 4px; font-size: 10px;">Max Actions/Hour:</label>
                <input type="number" id="ppMaxActions" value="${ppSettings.maxActions}" min="10" max="200" style="width: 100%; padding: 6px; background: #333; color: white; border: 1px solid #555; border-radius: 4px; font-size: 10px;">
            </div>
            <div style="display: flex; align-items: end;">
                <label style="display: flex; align-items: center; cursor: pointer;">
                    <input type="checkbox" id="ppSafetyMode" ${ppSettings.safetyMode ? 'checked' : ''}>
                    <span style="margin-left: 6px; font-size: 10px;">Safety Mode</span>
                </label>
            </div>
        </div>

        <button id="ppTestPick" style="width: 100%; padding: 8px; background: linear-gradient(45deg, #37b24d, #51cf66); border: none; color: white; border-radius: 6px; cursor: pointer; font-size: 11px; font-weight: bold; box-shadow: 0 2px 8px rgba(55,178,77,0.3); transition: all 0.2s;">🧪 Test Auto Pick</button>
    `;

    function setupEventHandlers() {
        // Coloring toggle
        const coloringToggle = document.getElementById('ppColoringEnabled');
        if (coloringToggle) {
            coloringToggle.onchange = function() {
                ppSettings.coloringEnabled = this.checked;
                saveSettings();
                setTimeout(processCurrentTargets, 100);
                log(`Color coding ${this.checked ? 'enabled' : 'disabled'}`, 'info');
            };
        }

        // Auto picker toggle
        const autoPickToggle = document.getElementById('ppAutoPickerEnabled');
        if (autoPickToggle) {
            autoPickToggle.onchange = function() {
                ppSettings.autoPickerEnabled = this.checked;

                const status = document.getElementById('ppAutoPickStatus');
                if (status) {
                    status.textContent = ppSettings.autoPickerEnabled ? 'ACTIVE' : 'DISABLED';
                    status.style.background = ppSettings.autoPickerEnabled ? '#37b24d' : '#666';
                }

                if (ppSettings.autoPickerEnabled && isOnPickpocketPage() && !isRateLimited()) {
                    startAutoPicker();
                } else {
                    stopAutoPicker();
                }

                saveSettings();
                log(`Auto picker ${this.checked ? 'enabled' : 'disabled'}`, 'info');
            };
        }

        // Number inputs
        ['ppAutoPickDelay', 'ppMaxNerve', 'ppMaxActions'].forEach(id => {
            const input = document.getElementById(id);
            if (input) {
                input.onchange = function() {
                    const setting = id.replace('pp', '').charAt(0).toLowerCase() + id.replace('pp', '').slice(1);
                    ppSettings[setting] = parseInt(this.value);
                    saveSettings();

                    // Restart auto picker if running and delay changed
                    if (id === 'ppAutoPickDelay' && autoPickerInterval) {
                        stopAutoPicker();
                        startAutoPicker();
                    }
                };
            }
        });

        // Safety mode toggle
        const safetyToggle = document.getElementById('ppSafetyMode');
        if (safetyToggle) {
            safetyToggle.onchange = function() {
                ppSettings.safetyMode = this.checked;
                saveSettings();
                log(`Safety mode ${this.checked ? 'enabled' : 'disabled'}`, 'info');
            };
        }

        // Category checkboxes
        document.querySelectorAll('.ppCategoryCheck').forEach(checkbox => {
            checkbox.onchange = () => {
                ppSettings.targetCategories = Array.from(document.querySelectorAll('.ppCategoryCheck:checked')).map(cb => cb.value);
                saveSettings();
                log(`Target categories updated: ${ppSettings.targetCategories.join(', ')}`, 'info');
            };
        });

        // Activity checkboxes
        document.querySelectorAll('.ppActivityCheck').forEach(checkbox => {
            checkbox.onchange = () => {
                ppSettings.targetActivities = Array.from(document.querySelectorAll('.ppActivityCheck:checked')).map(cb => cb.value);
                saveSettings();
                log(`Target activities updated: ${ppSettings.targetActivities.join(', ')}`, 'info');
            };
        });

        // Test button
        const testBtn = document.getElementById('ppTestPick');
        if (testBtn) {
            testBtn.onclick = () => {
                log('Testing auto pick functionality...', 'warning');
                const result = performAutoPick(true);
                if (!result) {
                    log('Test failed - check your target settings or visit crimes page', 'error');
                }
            };
        }
    }

    // Module configuration
    const moduleConfig = {
        name: 'Pickpocket',
        version: '1.1',
        description: 'Enhanced pickpocketing with color coding and auto-picker',
        menuSection: menuSection,
        initialize: function() {
            log('Pickpocket module initializing...', 'info');

            setupEventHandlers();

            // Start observer for target processing
            const observer = new MutationObserver(() => {
                if (ppSettings.coloringEnabled) {
                    clearTimeout(observer.debounceTimer);
                    observer.debounceTimer = setTimeout(processCurrentTargets, 300);
                }
            });
            observer.observe(document.body, { childList: true, subtree: true });

            // Initial target processing
            setTimeout(processCurrentTargets, 1000);

            // Auto-start if enabled and on pickpocket page
            if (ppSettings.autoPickerEnabled && isOnPickpocketPage()) {
                setTimeout(startAutoPicker, 2000);
                log('Auto picker will start in 2 seconds (on pickpocket page)', 'info');
            }

            log('Pickpocket module initialized successfully', 'success');
        },
        cleanup: function() {
            stopAutoPicker();
            log('Pickpocket module cleaned up', 'info');
        },
        isActive: function() {
            return ppSettings.autoPickerEnabled && autoPickerInterval !== null && isOnPickpocketPage();
        }
    };

    function initializePickpocketModule() {
        log('Attempting to register pickpocket module with framework...', 'info');

        // Register with framework
        if (globalWindow.TornFramework.registerModule(moduleConfig)) {
            log('Pickpocket module registered successfully', 'success');

            // Page change detection
            let currentUrl = window.location.href;
            const urlObserver = new MutationObserver(() => {
                if (window.location.href !== currentUrl) {
                    currentUrl = window.location.href;

                    if (!isOnPickpocketPage() && autoPickerInterval) {
                        stopAutoPicker();
                        log('Left pickpocket page - auto picker stopped', 'info');
                    } else if (isOnPickpocketPage() && ppSettings.autoPickerEnabled && !autoPickerInterval) {
                        setTimeout(startAutoPicker, 1000);
                        log('Entered pickpocket page - auto picker starting', 'info');
                    }
                }
            });

            urlObserver.observe(document.body, { childList: true, subtree: true });
        } else {
            log('Failed to register pickpocket module', 'error');
        }
    }

    // Wait for framework
    function waitForFramework() {
        console.log('[PICKPOCKET] Checking for framework...');
        if (globalWindow.TornFramework && globalWindow.TornFramework.initialized) {
            console.log('[PICKPOCKET] Framework found, initializing module');
            initializePickpocketModule();
        } else {
            console.log('[PICKPOCKET] Framework not ready, waiting...');
            setTimeout(waitForFramework, 500);
        }
    }

    // Start initialization
    waitForFramework();

})();