您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Dynamically numbers duplicate OC roles based on slot order, shows priority of roles, and stores your CPR for each crime/role
// ==UserScript== // @name OC Role Display - PERK_Ryan Edition // @namespace http://tampermonkey.net/ // @version 1.5.0 // @description Dynamically numbers duplicate OC roles based on slot order, shows priority of roles, and stores your CPR for each crime/role // @author PERK_Ryan (made from Allenone and NotIbbyz work) // @match https://www.torn.com/factions.php?step=your* // @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com // @grant GM_info // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @license MIT // @connect tornprobability.com // ==/UserScript== //OC Role Display - PERK_Ryan Edition (async function() { 'use strict'; // Inject pulse animations const style = document.createElement('style'); style.innerHTML = ` @keyframes pulseRed { 0% { box-shadow: 0 0 8px red; } 50% { box-shadow: 0 0 18px red; } 100% { box-shadow: 0 0 8px red; } } .pulse-border-red { animation: pulseRed 1s infinite; } `; document.head.appendChild(style); const defaultCPR1 = 70; const defaultCPR2 = 70; const defaultCPR3 = 70; const ocRoles = [ { OCName: "Ace in the Hole", Positions: { "IMPERSONATOR": 70, "MUSCLE 1": 70, "MUSCLE 2": 70, "HACKER": 70, "DRIVER": 70 }, PositionPriority: { "IMPERSONATOR": "P5", "MUSCLE 1": "P3", "MUSCLE 2": "P4", "HACKER": "P1", "DRIVER": "P2" } }, { OCName: "Stacking the Deck", Positions: { "CAT BURGLAR": 70, "DRIVER": 70, "HACKER": 70, "IMPERSONATOR": 70 }, PositionPriority: { "CAT BURGLAR": "P2", "DRIVER": "P4", "HACKER": "P3", "IMPERSONATOR": "P1" } }, { OCName: "Break the Bank", Positions: { "ROBBER": 60, "MUSCLE 1": 60, "MUSCLE 2": 60, "THIEF 1": 60, "MUSCLE 3": 60, "THIEF 2": 60 }, PositionPriority: { "ROBBER": "P3", "MUSCLE 1": "P4", "MUSCLE 2": "P5", "THIEF 1": "P6", "MUSCLE 3": "P1", "THIEF 2": "P2" } }, { OCName: "Blast from the Past", Positions: { "PICKLOCK 1": 50, "HACKER": 50, "ENGINEER": 70, "BOMBER": 70, "MUSCLE": 70, "PICKLOCK 2": 40 }, PositionPriority: { "PICKLOCK 1": "P4", "HACKER": "P5", "ENGINEER": "P2", "BOMBER": "P3", "MUSCLE": "P1", "PICKLOCK 2": "P6" } }, { OCName: "Bidding War", Positions: { "ROBBER 1": 60, "DRIVER": 60, "ROBBER 2": 60, "ROBBER 3": 70, "BOMBER 1": 60, "BOMBER 2": 60 }, PositionPriority: { "ROBBER 1": "P6", "DRIVER": "P3", "ROBBER 2": "P2", "ROBBER 3": "P1", "BOMBER 1": "P5", "BOMBER 2": "P4" } }, { OCName: "Honey Trap", Positions: { "ENFORCER": 40, "MUSCLE 1": 60, "MUSCLE 2": 70 }, PositionPriority: { "ENFORCER": "P3", "MUSCLE 1": "P2", "MUSCLE 2": "P1" } }, { OCName: "No Reserve", Positions: { "CAR THIEF": 60, "TECHIE": 70, "ENGINEER": 60 }, PositionPriority: { "CAR THIEF": "P3", "TECHIE": "P1", "ENGINEER": "P2" } }, { OCName: "Leave No Trace", Positions: { "TECHIE": 60, "NEGOTIATOR": 60, "IMPERSONATOR": 60 }, PositionPriority: { "TECHIE": "P3", "NEGOTIATOR": "P2", "IMPERSONATOR": "P1" } }, { OCName: "Stage Fright", Positions: { "ENFORCER": 60, "MUSCLE 1": 60, "MUSCLE 2": 60, "MUSCLE 3": 60, "LOOKOUT": 60, "SNIPER": 70 }, PositionPriority: { "ENFORCER": "P3", "MUSCLE 1": "P2", "MUSCLE 2": "P6", "MUSCLE 3": "P4", "LOOKOUT": "P5", "SNIPER": "P1" } }, { OCName: "Snow Blind", Positions: { "HUSTLER": 60, "IMPERSONATOR": 60, "MUSCLE 1": 60, "MUSCLE 2": 60 }, PositionPriority: { "HUSTLER": "P1", "IMPERSONATOR": "P2", "MUSCLE 1": "P3", "MUSCLE 2": "P4" } }, { OCName: "Smoke and Wing Mirrors", Positions: { "CAR THIEF": 70, "IMPERSONATOR": 70, "HUSTLER 1": 50, "HUSTLER 2": 50 }, PositionPriority: { "CAR THIEF": "P1", "IMPERSONATOR": "P2", "HUSTLER 1": "P4", "HUSTLER 2": "P3" } }, { OCName: "Market Forces", Positions: { "ENFORCER": 60, "NEGOTIATOR": 60, "LOOKOUT": 60, "ARSONIST": 60, "MUSCLE": 60 }, PositionPriority: { "ENFORCER": "P1", "NEGOTIATOR": "P2", "LOOKOUT": "P4", "ARSONIST": "P5", "MUSCLE": "P3" } }, { OCName: "Best of the Lot", Positions: { "PICKLOCK": 60, "CAR THIEF": 60, "MUSCLE": 60, "IMPERSONATOR": 60 }, PositionPriority: { "PICKLOCK": "P2", "CAR THIEF": "P3", "MUSCLE": "P1", "IMPERSONATOR": "P4" } }, { OCName: "Cash Me if You Can", Positions: { "THIEF 1": 60, "THIEF 2": 60, "LOOKOUT": 60 }, PositionPriority: { "THIEF 1": "P1", "THIEF 2": "P3", "LOOKOUT": "P2" } }, { OCName: "Mob Mentality", Positions: { "LOOTER 1": 10, "LOOTER 2": 10, "LOOTER 3": 10, "LOOTER 4": 10 }, PositionPriority: { "LOOTER 1": "P1", "LOOTER 2": "P2", "LOOTER 3": "P4", "LOOTER 4": "P3" } }, { OCName: "Pet Project", Positions: { "KIDNAPPER": 10, "MUSCLE": 10, "PICKLOCK": 10 }, PositionPriority: { "KIDNAPPER": "P1", "MUSCLE": "P3", "PICKLOCK": "P2" } } ]; const roleMappings = {}; function processScenario(panel) { if (panel.classList.contains('role-processed')) return; panel.classList.add('role-processed'); const ocName = panel.querySelector('.panelTitle___aoGuV')?.innerText.trim() || "Unknown"; const slots = panel.querySelectorAll('.wrapper___Lpz_D'); if (!roleMappings[ocName]) { const slotsWithPos = Array.from(slots).map(slot => { const fiberKey = Object.keys(slot).find(k => k.startsWith('__reactFiber$')); if (!fiberKey) return null; const fiberNode = slot[fiberKey]; const slotKey = fiberNode.return.key.replace('slot-', ''); const posNum = parseInt(slotKey.match(/P(\d+)/)?.[1] || '0', 10); return { slot, positionNumber: posNum }; }).filter(Boolean); slotsWithPos.sort((a, b) => a.positionNumber - b.positionNumber); const originalNames = slotsWithPos.map(({ slot }) => slot.querySelector('.title___UqFNy')?.innerText.trim() || 'Unknown' ); const freq = {}; originalNames.forEach(name => { freq[name] = (freq[name] || 0) + 1; }); const finalNames = []; const counter = {}; originalNames.forEach(name => { if (freq[name] > 1) { counter[name] = (counter[name] || 0) + 1; finalNames.push(`${name} ${counter[name]}`); } else { finalNames.push(name); } }); roleMappings[ocName] = finalNames; } Array.from(slots).forEach(slot => { const fiberKey = Object.keys(slot).find(k => k.startsWith('__reactFiber$')); if (!fiberKey) return; const fiberNode = slot[fiberKey]; const positionKey = fiberNode.return.key.replace('slot-', ''); const posNum = parseInt(positionKey.match(/P(\d+)/)?.[1] || '0', 10); const roleIndex = posNum - 1; const finalName = roleMappings[ocName][roleIndex]; const successStr = slot.querySelector('.successChance___ddHsR')?.textContent.trim() || ''; const successChance = parseInt(successStr, 10) || 0; const joinBtn = slot.querySelector("button[class^='torn-btn joinButton']"); const ocData = ocRoles.find(o => o.OCName === ocName); let required = null; let priorityPrefix = ''; if (ocData) { if (typeof ocData.Positions === 'object') { const baseName = finalName.replace(/\s\d+$/, ''); if (ocData.Positions[finalName] !== undefined) { required = ocData.Positions[finalName]; } else if (ocData.Positions[baseName] !== undefined) { required = ocData.Positions[baseName]; } if (ocData.PositionPriority && ocData.PositionPriority[finalName]) { priorityPrefix = `${ocData.PositionPriority[finalName]} `; } } } if (required !== null) { const honorTextSpans = slot.querySelectorAll('.honor-text'); const userName = (honorTextSpans.length > 1) ? honorTextSpans[1].textContent.trim() : null; if (!userName) { slot.style.backgroundColor = (successChance < required) ? '#ff000061' : '#21a61c61'; } if (joinBtn) { if (successChance < required) { joinBtn.setAttribute('disabled', ''); } } if (userName && successChance < required) { slot.classList.add('pulse-border-red'); slot.style.outline = '4px solid red'; slot.style.outlineOffset = '0px'; } } const roleElem = slot.querySelector('.title___UqFNy'); if (finalName && roleElem) { const priority = ocData?.PositionPriority?.[finalName] || 'idk'; const updatedName = `${priority ? priority + ' ' : ''}${finalName}`; roleElem.innerText = updatedName; } }); } const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.addedNodes.length) { mutation.addedNodes.forEach(node => { if (node.nodeType === 1 && node.matches('.wrapper___U2Ap7')) { processScenario(node); } if (node.nodeType === 1) { const innerPanels = node.querySelectorAll?.('.wrapper___U2Ap7') || []; innerPanels.forEach(inner => processScenario(inner)); } }); } }); }); const targetNode = document.querySelector('#factionCrimes-root') || document.body; observer.observe(targetNode, { childList: true, subtree: true }); window.addEventListener('load', () => { document.querySelectorAll('.wrapper___U2Ap7').forEach(panel => { processScenario(panel); }); }); })(); /* Faction CPR Tracker */ (function() { 'use strict'; const API_ENDPOINT = 'https://tornprobability.com:3000/api/SubmitCPR'; const TARGET_URL_BASE = 'page.php?sid=organizedCrimesData&step=crimeList'; const STORAGE_PREFIX = 'FactionCPRTracker_'; const isTornPDA = typeof window.flutter_inappwebview !== 'undefined'; // Storage functions const getValue = isTornPDA ? (key, def) => JSON.parse(localStorage.getItem(key) || JSON.stringify(def)) : GM_getValue; const setValue = isTornPDA ? (key, value) => localStorage.setItem(key, JSON.stringify(value)) : GM_setValue; // HTTP request function const xmlhttpRequest = isTornPDA ? (details) => { window.flutter_inappwebview.callHandler('PDA_httpPost', details.url, details.headers, details.data) .then(response => { details.onload({ status: response.status, responseText: response.data }); }) .catch(err => details.onerror(err)); } : GM_xmlhttpRequest; // API key handling let API_KEY; if (isTornPDA) { API_KEY = "###PDA-APIKEY###"; // Hardcoded for TornPDA Change this manually if you want to use a different key. setValue(`${STORAGE_PREFIX}api_key`, API_KEY); } else { API_KEY = getValue(`${STORAGE_PREFIX}api_key`, null); if (!API_KEY) { API_KEY = prompt('Please enter your Torn API key (Public Access):'); if (!API_KEY) { alert('Faction CPR Tracker: API key is required for functionality.'); return; } setValue(`${STORAGE_PREFIX}api_key`, API_KEY); if (API_KEY) { submitCPRData(API_KEY, getValue(`${STORAGE_PREFIX}CheckpointPassRates`, {})); } } } async function submitCPRData(apiKey, checkpointPassRates) { return new Promise((resolve, reject) => { xmlhttpRequest({ method: 'POST', url: API_ENDPOINT, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ apiKey, checkpointPassRates }), onload: (response) => { if (response.status >= 200 && response.status < 300) { console.log('CPR data submitted successfully'); resolve(); } else { console.error('API error:', response.status, response.responseText); reject(new Error('API error')); } }, onerror: (err) => { console.error('Submission error:', err); reject(err); } }); }); } function processCPRs(data) { const scenarioName = data.scenario.name; let CheckpointPassRates = getValue(`${STORAGE_PREFIX}CheckpointPassRates`, {}); if (!CheckpointPassRates[scenarioName]) { CheckpointPassRates[scenarioName] = {}; data.playerSlots.forEach(slot => { CheckpointPassRates[scenarioName][slot.name] = slot.player === null ? slot.successChance : 0; }); } else { data.playerSlots.forEach(slot => { if (slot.player === null) { CheckpointPassRates[scenarioName][slot.name] = slot.successChance; } }); } setValue(`${STORAGE_PREFIX}CheckpointPassRates`, CheckpointPassRates); } // Fetch Interception const win = isTornPDA ? window : (typeof unsafeWindow !== 'undefined' ? unsafeWindow : window); const originalFetch = win.fetch; win.fetch = async function(resource, config) { const url = typeof resource === 'string' ? resource : resource.url; if (config?.method?.toUpperCase() !== 'POST' || !url.includes(TARGET_URL_BASE)) { return originalFetch.apply(this, arguments); } let isRecruitingGroup = false; if (config?.body instanceof FormData) { isRecruitingGroup = config.body.get('group') === 'Recruiting'; } else if (config?.body) { isRecruitingGroup = config.body.toString().includes('group=Recruiting'); } if (!isRecruitingGroup) { return originalFetch.apply(this, arguments); } const response = await originalFetch.apply(this, arguments); try { const json = JSON.parse(await response.clone().text()); if (json.success && json.data && json.data.length > 1) { json.data.forEach(processCPRs); const API_KEY = getValue(`${STORAGE_PREFIX}api_key`, null); if (API_KEY) { submitCPRData(API_KEY, getValue(`${STORAGE_PREFIX}CheckpointPassRates`, {})); } } } catch (err) { console.error('Error processing response:', err); } return response; }; })();