// ==UserScript==
// @name Torn Pickpocketing Helper (Enhanced & Optimized)
// @namespace torn.pickpocketing.helper.rebuilt.v15.2 // Updated namespace
// @version 15.6 // Updated version
// @description An optimized and robust helper for Torn City pickpocketing, merging Cyclist Ring and Pickpocketing Colors.
// @author eaksquad, Microbes, Korbrm
// @match https://www.torn.com/loader.php?sid=crimes*
// @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Use the version number from the metadata for internal logging and consistency.
const SCRIPT_VERSION = "15.6"; // Updated internal version to match metadata
console.log(`[PPHelper v${SCRIPT_VERSION}] Script loading.`);
// --- Configuration ---
// User-configurable settings for the script's features.
const SETTINGS = {
cyclistAlerts: {
enabled: true,
soundUrl: 'https://audio.jukehost.co.uk/gxd2HB9RibSHhr13OiW6ROCaaRbD8103', // Custom sound for cyclist appearance
highlightColor: '#00ff00', // Bright green for cyclist highlight
highlightOpacity: '0.3' // Transparency level for the highlight background
},
difficultyColors: {
enabled: true,
showCategoryText: true // Whether to append the difficulty category name (e.g., "(Safe)") to crime titles
},
// NEW: Setting to control hiding the Police Officer target.
hidePolice: {
enabled: true // Default to enabled, can be toggled by the button.
}
};
// --- Data Definitions ---
// Maps target names to difficulty categories. These are used for applying specific colors.
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"], // Specifically targets Cyclists for alerts.
"Very Dangerous": ["Police officer"] // Typically the highest risk category.
};
// Standard color mapping for each difficulty category.
const categoryColorMap = {
"Safe": "#37b24d", // Green
"Moderately Unsafe": "#74b816", // Light Green/Yellow
"Unsafe": "#f59f00", // Orange
"Risky": "#f76707", // Dark Orange
"Dangerous": "#f03e3e", // Red
"Very Dangerous": "#7048e8" // Purple (often for special or very high risk)
};
// Color mapping for borders, dynamically determined by the player's skill tiers.
const skillTiers = {
tier1: { "Safe": "#37b24d", "Moderately Unsafe": "#f76707", "Unsafe": "#f03e3e", "Risky": "#f03e3e", "Dangerous": "#f03e3e", "Very Dangerous": "#7048e8" },
tier2: { "Safe": "#37b24d", "Moderately Unsafe": "#37b24d", "Unsafe": "#f76707", "Risky": "#f03e3e", "Dangerous": "#f03e3e", "Very Dangerous": "#7048e8" },
tier3: { "Safe": "#37b24d", "Moderately Unsafe": "#37b24d", "Unsafe": "#37b24d", "Risky": "#f76707", "Dangerous": "#f03e3e", "Very Dangerous": "#7048e8" },
tier4: { "Safe": "#37b24d", "Moderately Unsafe": "#37b24d", "Unsafe": "#37b24d", "Risky": "#37b24d", "Dangerous": "#f76707", "Very Dangerous": "#7048e8" },
tier5: { "Safe": "#37b24d", "Moderately Unsafe": "#37b24d", "Unsafe": "#37b24d", "Risky": "#37b24d", "Dangerous": "#37b24d", "Very Dangerous": "#7048e8" }
};
// --- State Management ---
// Flags to track the enabled status of script features.
let isCyclistAlertsEnabled = SETTINGS.cyclistAlerts.enabled;
let isDifficultyColorsEnabled = SETTINGS.difficultyColors.enabled;
// NEW: State variable for hiding police officers.
let isHidePoliceEnabled = SETTINGS.hidePolice.enabled;
// State variables for managing the cyclist sound alert's cooldown and visibility.
let wasCyclistVisibleLastRun = false;
let lastCyclistCheckTime = 0;
// --- Enhanced Utility Functions ---
/**
* Helper function to debounce rapid calls to a function.
* Ensures a function is only executed after a specified period of inactivity.
* @param {Function} func - The function to debounce.
* @param {number} wait - The number of milliseconds to delay execution.
* @returns {Function} The debounced function.
*/
function debounce(func, wait) {
let timeout; // Stores the timeout ID
return function executedFunction(...args) {
// `this` context and arguments are preserved for the debounced function
const context = this;
const later = () => {
clearTimeout(timeout); // Clear the timeout so it doesn't run again
func.apply(context, args); // Execute the original function
};
clearTimeout(timeout); // Clear any previously scheduled execution
timeout = setTimeout(later, wait); // Schedule the execution
};
}
/**
* Plays the configured alert sound for cyclists.
* Includes a fallback mechanism to generate a simple beep using the Web Audio API if the audio file fails to play.
*/
function playAlertSound() {
try {
const audio = new Audio(SETTINGS.cyclistAlerts.soundUrl);
audio.volume = 0.7; // Set volume to 70% for clarity.
audio.play().catch(error => {
console.error(`[PPHelper v${SCRIPT_VERSION}] Audio playback failed:`, error);
// Fallback to a simple beep if audio playback fails.
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.type = 'sine'; // Use a sine wave for a pure tone.
oscillator.frequency.setValueAtTime(800, audioContext.currentTime); // Set frequency to 800 Hz.
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); // Set gain (volume) to 30%.
oscillator.start();
oscillator.stop(audioContext.currentTime + 0.5); // Stop the oscillator after 0.5 seconds.
} catch (beepError) {
console.error(`[PPHelper v${SCRIPT_VERSION}] Fallback beep generation also failed:`, beepError);
}
});
} catch (error) {
console.error(`[PPHelper v${SCRIPT_VERSION}] Error creating Audio object:`, error);
}
}
/**
* Waits for a specific DOM element to appear in the document.
* This is crucial for elements that are loaded asynchronously or after initial page rendering.
* @param {string} selector - The CSS selector for the element to wait for.
* @returns {Promise<Element>} A promise that resolves with the found element.
*/
function waitForElementToExist(selector) {
return new Promise(resolve => {
// Check if the element already exists.
const existingElement = document.querySelector(selector);
if (existingElement) {
return resolve(existingElement);
}
// If not, set up a MutationObserver to watch for changes.
const observer = new MutationObserver((mutations, obs) => {
mutations.forEach(mutation => {
// If nodes were added, check them.
if (mutation.type === 'childList' && mutation.addedNodes && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(node => {
// Ensure it's an element node and contains or is the target selector.
if (node.nodeType === 1) {
if (node.querySelector(selector)) {
resolve(node.querySelector(selector)); // Resolve with the found element.
obs.disconnect(); // Stop observing once found.
}
}
});
}
// Also check if the target node itself was modified and now matches.
if (mutation.target.nodeType === 1 && mutation.target.matches(selector)) {
resolve(mutation.target);
obs.disconnect();
}
});
});
// Start observing the entire document body for any changes.
observer.observe(document.body, { childList: true, subtree: true });
});
}
/**
* Intercepts network requests to specific URLs.
* This is used to detect when the game fetches crime data, signaling a good time to update styles.
* @param {string} urlPart - A substring to match in the response URL (e.g., "torn.com").
* @param {string} queryPart - A substring to match in the response URL's path/query (e.g., "/page.php?sid=crimesData").
* @param {Function} callback - A function to be called with the parsed JSON response data.
*/
function interceptFetch(urlPart, queryPart, callback) {
const originalFetch = window.fetch; // Store the original fetch function.
// Override window.fetch with our custom function.
window.fetch = function(...args) {
// Call the original fetch and process its response.
return originalFetch.apply(this, args).then(response => {
// Check if the response URL matches our criteria.
if (response.url.includes(urlPart) && response.url.includes(queryPart)) {
// Clone the response to ensure the original response remains available.
// Then, parse the cloned response as JSON.
response.clone().json().then(json => {
callback(json); // Pass the JSON data to the callback function.
}).catch(error => {
console.error(`[PPHelper v${SCRIPT_VERSION}] Intercepted fetch JSON parsing failed:`, error);
callback(null); // Call callback with null if JSON parsing fails.
});
}
return response; // Return the original response object.
});
};
}
/**
* Applies difficulty-based styling and cyclist highlighting to crime options,
* and hides the Police Officer target if enabled.
* This function is optimized to perform styling in a single pass over the crime elements,
* improving efficiency and responsiveness.
*/
function applyAllStyling() {
// Find the player's skill button to determine their current skill level.
const skillButton = document.querySelector('button[aria-label^="Skill:"]');
if (!skillButton) {
// If the skill button isn't found, we cannot determine skill tiers, so we exit early.
return;
}
const skillText = skillButton.getAttribute('aria-label');
const currentSkill = parseFloat(skillText.replace('Skill: ', '')); // Extract skill value.
// Determine the active color tier based on the player's current skill level.
let activeTierColors;
if (currentSkill < 10) { activeTierColors = skillTiers.tier1; }
else if (currentSkill < 35) { activeTierColors = skillTiers.tier2; }
else if (currentSkill < 65) { activeTierColors = skillTiers.tier3; }
else if (currentSkill < 80) { activeTierColors = skillTiers.tier4; }
else { activeTierColors = skillTiers.tier5; }
// Find all primary containers for crime options. These are the elements we will style or hide.
const allCrimeWrappers = document.querySelectorAll('div[class*="crimeOptionWrapper"]');
let isCyclistVisibleThisRun = false; // Flag to track if any cyclist is detected in the current update cycle.
// --- Single Pass Styling & Hiding: Iterate through each crime option wrapper ---
allCrimeWrappers.forEach(container => {
// --- Reset all previous styling applied by this script ---
// Resetting display to '' ensures elements are visible by default before the hiding logic is applied.
container.style.backgroundColor = '';
container.style.borderLeft = '';
container.style.boxShadow = '';
container.style.border = ''; // Reset any custom script border.
container.style.display = ''; // Reset display to ensure elements are visible by default.
container.classList.remove('cyclist-highlight'); // Remove the specific cyclist highlight class.
// Find the title element within the container, which typically holds the target's name.
const titleElement = container.querySelector('div[class*="titleAndProps"] > div');
if (!titleElement) {
// If a container lacks a title element, skip it.
return;
}
// Extract the base target name, stripping any category suffix (e.g., "(Safe)") that might have been added by previous runs of this script.
const originalTextFull = titleElement.textContent.trim();
const originalTextBase = originalTextFull.split(' (')[0];
// --- Police Officer Hiding Logic ---
// If hiding police is enabled and the target is a "Police officer", hide the entire container.
if (isHidePoliceEnabled && originalTextBase === "Police officer") {
container.style.display = 'none';
// Skip all further styling and checks for this hidden element.
return;
}
// --- Reset title element styles ---
titleElement.style.color = '';
titleElement.style.fontWeight = '';
titleElement.style.textShadow = '';
// --- Difficulty Coloring Logic ---
let category = null; // Variable to hold the determined difficulty category.
if (isDifficultyColorsEnabled) {
// Find the difficulty category for the current target name.
category = Object.keys(markGroups).find(cat => markGroups[cat].includes(originalTextBase));
if (category) {
// Apply text color based on the category's standard color map.
titleElement.style.color = categoryColorMap[category];
// Apply the left border color based on the player's skill tier and the target's category.
container.style.borderLeft = `3px solid ${activeTierColors[category]}`;
// Optionally append the category name to the title text if configured and the screen width is sufficient.
if (SETTINGS.difficultyColors.showCategoryText && window.innerWidth > 400) {
titleElement.textContent = `${originalTextBase} (${category})`;
} else {
// Otherwise, ensure only the base target name is displayed.
titleElement.textContent = originalTextBase;
}
} else {
// If the target name is not found in any category, ensure it displays only its base name.
titleElement.textContent = originalTextBase;
}
} else {
// If difficulty colors are globally disabled, ensure only the base target name is displayed.
titleElement.textContent = originalTextBase;
}
// --- Cyclist Highlighting Logic ---
// Determine if the current target is a cyclist. This check prioritizes exact matches and falls back to broader string checks.
const isCurrentTargetACyclist = (
originalTextBase.toLowerCase() === 'cyclist' || // Exact match for "Cyclist".
originalTextBase.toLowerCase().includes('cyclist') || // Check if "cyclist" is part of the name.
container.textContent.toLowerCase().includes('cyclist') // Broader fallback: check entire container text.
);
if (isCurrentTargetACyclist) {
isCyclistVisibleThisRun = true; // Mark that at least one cyclist has been found in this scan.
if (isCyclistAlertsEnabled) {
// Apply specific, prominent styles for cyclist highlights.
container.style.backgroundColor = SETTINGS.cyclistAlerts.highlightColor + SETTINGS.cyclistAlerts.highlightOpacity;
container.style.boxShadow = `0 0 15px ${SETTINGS.cyclistAlerts.highlightColor}`;
container.style.border = `2px solid ${SETTINGS.cyclistAlerts.highlightColor}`; // Override border if it was set by difficulty coloring.
container.classList.add('cyclist-highlight'); // Add the CSS class which includes the pulsating animation.
// Enhance the title text for better visibility.
titleElement.style.fontWeight = 'bold';
titleElement.style.textShadow = `0 0 5px ${SETTINGS.cyclistAlerts.highlightColor}`;
}
}
});
// --- Sound Alert Logic ---
// Trigger the sound alert if:
// 1. Cyclist alerts are enabled.
// 2. A cyclist was visible in the current update cycle (`isCyclistVisibleThisRun`).
// 3. No cyclist was visible in the previous run (`!wasCyclistVisibleLastRun`), indicating a new appearance.
// 4. The cooldown period has passed since the last alert was triggered.
const currentTime = Date.now();
if (isCyclistAlertsEnabled && isCyclistVisibleThisRun && !wasCyclistVisibleLastRun) {
if (currentTime - lastCyclistCheckTime > 2000) { // Enforce a 2-second cooldown to prevent rapid, annoying alerts.
console.log(`[PPHelper v${SCRIPT_VERSION}] Cyclist has appeared! Playing sound.`);
playAlertSound(); // Play the alert sound.
lastCyclistCheckTime = currentTime; // Record the time of this alert.
}
}
// Update the state variable for the next cycle to track visibility continuity.
wasCyclistVisibleLastRun = isCyclistVisibleThisRun;
}
/**
* Initializes the DOM observer and periodic checks to detect changes
* and trigger styling updates efficiently.
*/
function initializeCrimeObserver() {
// Method 1: Intercept fetch requests for `crimesData`.
// This is often the most reliable trigger as the game explicitly fetches this data when crime lists update.
interceptFetch("torn.com", "/page.php?sid=crimesData", () => {
console.log(`[PPHelper v${SCRIPT_VERSION}] Intercepted crimesData, scheduling style update.`);
// Use setTimeout to allow the DOM to potentially update after fetch success before styling.
// The debounced call prevents multiple rapid updates if multiple fetches occur.
debouncedApplyAllStyling();
});
// Method 2: Use a MutationObserver to watch for changes in the DOM.
// This catches dynamic content additions that might not directly correspond to a fetch call.
const observer = new MutationObserver((mutations) => {
let shouldUpdate = false;
// Iterate through all mutations detected.
mutations.forEach((mutation) => {
// We are primarily interested in nodes that were added to the DOM.
if (mutation.type === 'childList' && mutation.addedNodes && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
// Check if the added node is an element and if it, or its descendants, contain elements relevant to the script.
if (node.nodeType === 1) { // nodeType 1 indicates an Element node.
// Heuristic check: if the node contains crime wrappers, relevant buttons, or the keyword 'cyclist',
// it's a strong indication that the crime list has been updated.
if (node.querySelector('div[class*="crimeOptionWrapper"]') ||
node.querySelector('button[aria-label="Pickpocket, 5 nerve"]') ||
node.textContent.toLowerCase().includes('cyclist')) {
shouldUpdate = true;
}
}
});
}
});
if (shouldUpdate) {
console.log(`[PPHelper v${SCRIPT_VERSION}] DOM mutation detected, scheduling style update.`);
// Call the debounced version of applyAllStyling to prevent rapid, repeated executions.
debouncedApplyAllStyling();
}
});
// Start observing the entire document body for changes.
// `childList: true` observes direct children, `subtree: true` observes all descendants.
// This ensures comprehensive detection of dynamic content changes.
observer.observe(document.body, {
childList: true,
subtree: true,
// characterData: true // Generally not needed unless text content changes directly without node replacement.
});
// Method 3: Periodic check as a fallback.
// This serves as a safety net to ensure styling is applied even if observers somehow miss an update or if the page state doesn't trigger them.
// It runs every 5 seconds.
setInterval(() => {
// Only perform the check if relevant elements are present on the page, to avoid unnecessary operations.
if (document.querySelector('div[class*="crimeOptionWrapper"]')) {
console.log(`[PPHelper v${SCRIPT_VERSION}] Periodic check triggered, applying styles.`);
// Use the debounced function for the periodic check as well.
debouncedApplyAllStyling();
}
}, 5000);
}
/**
* Sets up the user interface elements (buttons) for controlling the script's features.
* This function has been refactored to use vanilla JavaScript, removing the jQuery dependency.
* The "Test Sound" button is repurposed to toggle the "Hide Police" functionality.
*/
function setupInterface() {
// Wait for the main container element of the pickpocketing section to ensure it has loaded.
waitForElementToExist('.pickpocketing-root').then((pickpocketingRoot) => {
// Prevent re-adding controls if they already exist (e.g., during rapid reloads or script updates).
if (document.getElementById('pp-helper-controls')) return;
// HTML structure for the control panel containing toggle buttons.
// The 'Test Sound' button is repurposed for controlling the 'Hide Police' feature.
const controlsContainerHTML = `
<div id="pp-helper-controls" style="margin-bottom: 10px; display: flex; gap: 10px; flex-wrap: nowrap; overflow-x: auto; padding-bottom: 5px;">
<button id="cyclist-toggle-btn" class="torn-btn" style="min-width: 120px;">Cyclist Alerts: ON</button>
<button id="colors-toggle-btn" class="torn-btn" style="min-width: 120px;">Risk Colors: ON</button>
<button id="police-hide-btn" class="torn-btn" style="background: #2196F3; color: white; min-width: 120px;">Hide Police: ON</button>
</div>
`;
// Insert the HTML for the controls into the page using a native DOM method.
pickpocketingRoot.insertAdjacentHTML('afterbegin', controlsContainerHTML);
// Get references to the newly created buttons using native DOM methods.
const cyclistBtn = document.getElementById('cyclist-toggle-btn');
const colorsBtn = document.getElementById('colors-toggle-btn');
// Renamed button for its new function
const policeHideBtn = document.getElementById('police-hide-btn');
/**
* Updates the text and visual styling of the toggle buttons to accurately reflect their current enabled state.
*/
function updateButtons() {
// Update text content for cyclist alerts.
cyclistBtn.textContent = `Cyclist Alerts: ${isCyclistAlertsEnabled ? 'ON' : 'OFF'}`;
cyclistBtn.style.background = isCyclistAlertsEnabled ? '#4CAF50' : ''; // Green when ON.
cyclistBtn.style.color = isCyclistAlertsEnabled ? 'white' : '';
// Update text content for difficulty colors.
colorsBtn.textContent = `Difficulty Colors: ${isDifficultyColorsEnabled ? 'ON' : 'OFF'}`;
colorsBtn.style.background = isDifficultyColorsEnabled ? '#4CAF50' : ''; // Green when ON.
colorsBtn.style.color = isDifficultyColorsEnabled ? 'white' : '';
// Update text content and styling for the police hiding button.
policeHideBtn.textContent = `Hide Police: ${isHidePoliceEnabled ? 'ON' : 'OFF'}`;
policeHideBtn.style.background = isHidePoliceEnabled ? '#4CAF50' : ''; // Green when ON.
policeHideBtn.style.color = isHidePoliceEnabled ? 'white' : '';
}
/**
* Attempts to trigger a page refresh or directly re-applies styles.
* This is useful to ensure that UI changes (like toggling features) are immediately visible.
*/
function forceRefreshOrApplyStyles() {
// Try to find and click the game's built-in refresh button for a natural refresh.
// The selector for the refresh button might change with Torn updates.
const refreshButton = document.querySelector('div[class*="refresh-icon_"]'); // Example selector.
if (refreshButton) {
refreshButton.click();
} else {
// If the refresh button is not found, directly re-apply all styles.
console.log(`[PPHelper v${SCRIPT_VERSION}] Refresh button not found, forcing style re-application.`);
// Use the debounced function for safety against rapid calls.
debouncedApplyAllStyling();
}
}
// Add event listeners to the buttons for user interaction.
cyclistBtn.addEventListener('click', () => {
isCyclistAlertsEnabled = !isCyclistAlertsEnabled; // Toggle the cyclist alerts setting.
updateButtons(); // Update the button's appearance.
forceRefreshOrApplyStyles(); // Apply the new setting to the crime list.
});
colorsBtn.addEventListener('click', () => {
isDifficultyColorsEnabled = !isDifficultyColorsEnabled; // Toggle the difficulty colors setting.
updateButtons(); // Update the button's appearance.
forceRefreshOrApplyStyles(); // Apply the new setting to the crime list.
});
// NEW EVENT LISTENER: Repurposed button toggles Police hiding.
policeHideBtn.addEventListener('click', () => {
isHidePoliceEnabled = !isHidePoliceEnabled; // Toggle the hide police setting.
updateButtons(); // Update the button's appearance.
// Re-applying styles will now hide/show the police officer based on the new setting.
forceRefreshOrApplyStyles();
});
// Initialize the buttons' appearance based on current settings.
updateButtons();
// Apply styling to the crime list initially. A slight delay helps ensure the DOM is fully ready.
setTimeout(debouncedApplyAllStyling, 500);
});
}
// --- Script Entry Point ---
console.log(`[PPHelper v${SCRIPT_VERSION}] Initializing...`);
// Define the debounced styling function here so it's available globally within the script's scope.
const debouncedApplyAllStyling = debounce(applyAllStyling, 200); // 200ms debounce delay.
// Ensure the script logic executes only after the DOM is fully loaded and parsed.
if (document.readyState === 'loading') {
// If the document is still loading, wait for the 'DOMContentLoaded' event.
document.addEventListener('DOMContentLoaded', () => {
setupInterface(); // Set up the user interface controls.
initializeCrimeObserver(); // Initialize the DOM observers and periodic checks.
});
} else {
// If the document is already loaded (e.g., script injected after page load), proceed immediately.
setupInterface();
initializeCrimeObserver();
}
// Inject CSS for cyclist highlighting animation and smoother style transitions.
const style = document.createElement('style');
style.textContent = `
/* Keyframes for the pulsating animation on cyclist highlights. */
@keyframes cyclistPulse {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
/* Apply the animation to elements with the cyclist-highlight class. */
.cyclist-highlight {
animation: cyclistPulse 2s infinite;
}
/* Add smooth transitions to crime option wrappers for visual feedback when styles change. */
div[class*="crimeOptionWrapper"] {
transition: background-color 0.3s ease, border-left 0.3s ease, box-shadow 0.3s ease, display 0.1s ease-in-out; /* Added display transition for smoother hiding/showing */
}
/* Basic styling for control panel buttons to ensure consistency. */
#pp-helper-controls button.torn-btn {
cursor: pointer; /* Indicates the button is clickable. */
padding: 6px 12px; /* Standard padding for buttons. */
border-radius: 4px; /* Slightly rounded corners. */
border: 1px solid #555; /* Default border. */
background-color: #3a3a3a; /* Dark background, typical for Torn UI elements. */
color: #eee; /* Light text color. */
font-size: 0.9em; /* Readable font size. */
display: inline-flex; /* Helps align content within the button. */
align-items: center; /* Vertically center text. */
justify-content: center; /* Horizontally center text. */
flex-shrink: 0; /* Prevent buttons from shrinking, ensuring they stay in one row */
}
#pp-helper-controls button.torn-btn:hover {
background-color: #555; /* Slightly lighter background on hover. */
}
`;
document.head.appendChild(style);
console.log(`[PPHelper v${SCRIPT_VERSION}] Script fully loaded and initialized.`);
})();