您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Calculate and display ranked war cache reward values with % of total reward split, custom pricing, API integration, PDA support, and other rewards (points/respect). Features automatic theme detection and multiple trader configurations.
// ==UserScript== // @name Ranked War Cache Rewards Value // @namespace http://tampermonkey.net/ // @version 3.1 // @description Calculate and display ranked war cache reward values with % of total reward split, custom pricing, API integration, PDA support, and other rewards (points/respect). Features automatic theme detection and multiple trader configurations. // @author Mistborn [3037268] // @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com // @match https://www.torn.com/war.php?step=rankreport* // @run-at document-start // @grant GM.xmlHttpRequest // @grant none // @connect api.torn.com // @supportURL https://github.com/MistbornTC/ranked-war-cache-rewards/issues // @license MIT // @credits Inspired by bot_7420 [2937420] // ==/UserScript== // PDA API Key placeholder - MUST be at global scope for PDA replacement const PDA_API_KEY = '###PDA-APIKEY###'; // PDA API URLs - complete strings for PDA to process const PDA_USER_API_URL = 'https://api.torn.com/user/?selections=basic&key=###PDA-APIKEY###'; const PDA_ITEMS_API_URL = 'https://api.torn.com/torn/?selections=items&key=###PDA-APIKEY###'; const PDA_POINTS_API_URL = 'https://api.torn.com/torn/?selections=pointsmarket&key=###PDA-APIKEY###'; (function () { "use strict"; console.log("RWAwardValue: Script starting v3.1"); // Enhanced PDA detection function isTornPDA() { const userAgentCheck = navigator.userAgent.includes('com.manuito.tornpda'); const flutterCheck = !!window.flutter_inappwebview; const platformCheck = !!window.__PDA_platformReadyPromise; const httpCheck = !!window.PDA_httpGet; const result = !!(userAgentCheck || flutterCheck || platformCheck || httpCheck); console.log('RW: PDA Detection:', result, { userAgent: userAgentCheck, flutter: flutterCheck, platform: platformCheck, httpGet: httpCheck }); return result; } // Configure PDA session const PDA_MODE = isTornPDA(); console.log('RW: PDA Mode:', PDA_MODE); // ======================================== // GLOBAL VARIABLES & SETTINGS // ======================================== // User's API key and validation status let API_KEY = 'YOUR_API_KEY_HERE'; let API_USERNAME = ''; // Username from successful API validation let PDA_VALIDATED = false; // Whether PDA mobile app API is working let SETTINGS = { showApiValues: false, showIndirectRewards: false, showCustomPrices: false, respectValue: 20000 }; // Cache API prices with timestamp to avoid repeated calls let apiPriceCache = { lastFetched: null, data: {} }; // Debug mode - window.rwDebugMode = true/false let DEBUG_MODE = localStorage.getItem('rw_debug_mode') === 'true'; Object.defineProperty(window, 'rwDebugMode', { get() { return DEBUG_MODE; }, set(value) { DEBUG_MODE = !!value; localStorage.setItem('rw_debug_mode', DEBUG_MODE.toString()); console.log('RW: Debug mode', DEBUG_MODE ? 'ENABLED' : 'DISABLED'); } }); // Custom seller prices (up to 10 different sellers) - declared before loadSettings let sellerData = { activeSeller: 0, sellers: [ { name: "Trader 1", pricingMode: "fixed", prices: { armorCache: 0, heavyArmsCache: 0, mediumArmsCache: 0, meleeCache: 0, smallArmsCache: 0 } }, { name: "Trader 2", pricingMode: "fixed", prices: { armorCache: 0, heavyArmsCache: 0, mediumArmsCache: 0, meleeCache: 0, smallArmsCache: 0 } }, { name: "Trader 3", pricingMode: "fixed", prices: { armorCache: 0, heavyArmsCache: 0, mediumArmsCache: 0, meleeCache: 0, smallArmsCache: 0 } }, { name: "Trader 4", pricingMode: "fixed", prices: { armorCache: 0, heavyArmsCache: 0, mediumArmsCache: 0, meleeCache: 0, smallArmsCache: 0 } }, { name: "Trader 5", pricingMode: "fixed", prices: { armorCache: 0, heavyArmsCache: 0, mediumArmsCache: 0, meleeCache: 0, smallArmsCache: 0 } }, { name: "Trader 6", pricingMode: "fixed", prices: { armorCache: 0, heavyArmsCache: 0, mediumArmsCache: 0, meleeCache: 0, smallArmsCache: 0 } }, { name: "Trader 7", pricingMode: "fixed", prices: { armorCache: 0, heavyArmsCache: 0, mediumArmsCache: 0, meleeCache: 0, smallArmsCache: 0 } }, { name: "Trader 8", pricingMode: "fixed", prices: { armorCache: 0, heavyArmsCache: 0, mediumArmsCache: 0, meleeCache: 0, smallArmsCache: 0 } }, { name: "Trader 9", pricingMode: "fixed", prices: { armorCache: 0, heavyArmsCache: 0, mediumArmsCache: 0, meleeCache: 0, smallArmsCache: 0 } }, { name: "Trader 10", pricingMode: "fixed", prices: { armorCache: 0, heavyArmsCache: 0, mediumArmsCache: 0, meleeCache: 0, smallArmsCache: 0 } } ] }; // Working data for calculations let marketValueMap = new Map(); // Live market prices from API let pointMarketValue = 0; // Current points market rate let currentTheme = getTheme(); // Track dark/light mode let rewardData = []; // Processed reward data let rawRewardData = []; // Original data for recalculation // ======================================== // THEME DETECTION & MONITORING // ======================================== // Watch for theme changes and update UI automatically function monitorThemeChanges() { // Set up listener to detect when user switches between dark/light mode const observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.type === 'attributes' && (mutation.attributeName === 'class' || mutation.attributeName === 'style')) { const newTheme = getTheme(); if (newTheme !== currentTheme) { console.log('RW: Theme changed from', currentTheme, 'to', newTheme); currentTheme = newTheme; updateAllUIForTheme(); } } }); }); // Start observing body for class and style changes if (document.body) { observer.observe(document.body, { attributes: true, attributeFilter: ['class', 'style'] }); } } // Refresh all UI elements when theme changes function updateAllUIForTheme() { console.log('RW: Updating all UI elements for theme:', currentTheme); // Refresh the main reward display if (typeof refreshRewardDisplay === 'function') { refreshRewardDisplay(); } // Update any open configuration panels by refreshing their styles const pricePanel = document.getElementById('rw-price-panel'); const settingsPanel = document.getElementById('rw-settings-panel'); if (pricePanel && pricePanel.style.maxHeight !== '0px') { console.log('RW: Refreshing price panel colors for new theme'); // Close and reopen the price panel to apply new theme const priceButton = document.getElementById('rw-price-btn-header'); if (priceButton) { priceButton.click(); // Close setTimeout(() => priceButton.click(), 50); // Reopen with new theme } } if (settingsPanel && settingsPanel.style.maxHeight !== '0px') { console.log('RW: Refreshing settings panel colors for new theme'); // Close and reopen the settings panel to apply new theme const settingsButton = document.getElementById('rw-settings-btn-header'); if (settingsButton) { settingsButton.click(); // Close setTimeout(() => settingsButton.click(), 50); // Reopen with new theme } } } // ======================================== // UTILITY/HELPER FUNCTIONS // ======================================== function isMobile() { const width = window.innerWidth; const isMobileWidth = width <= 768; console.log('RW: Mobile detection - width:', width, 'isMobile:', isMobileWidth); return isMobileWidth; } // Mobile-safe arrow function using HTML entities function getMobileArrow(isPositive) { // Use HTML entities that work reliably across all platforms return isPositive ? '▲' : '▼'; // ▲ ▼ as HTML entities } // Mobile-safe expand arrow using HTML entities function getExpandArrow(isExpanded) { // Use HTML entities for consistent rendering return isExpanded ? '▲' : '▼'; // ▲ ▼ as HTML entities } function getTheme() { // Wait for body to exist if (!document.body) { return 'dark'; // Default fallback } const body = document.body; const isDarkMode = body.classList.contains('dark-mode') || body.classList.contains('dark') || body.style.background.includes('#191919') || getComputedStyle(body).backgroundColor === 'rgb(25, 25, 25)'; return isDarkMode ? 'dark' : 'light'; } function getThemeColors() { const theme = getTheme(); if (theme === 'dark') { return { panelBg: '#2a2a2a', panelBorder: '#444', statBoxBg: '#3a3a3a', statBoxBorder: '#444', statBoxShadow: '0 1px 3px rgba(0,0,0,0.1)', textPrimary: '#fff', textSecondary: '#ccc', textMuted: '#999', success: '#5cb85c', danger: '#d9534f', primary: 'rgb(116, 192, 252)', configBg: '#333', configBorder: '#555', inputBg: '#444', inputBorder: '#555' }; } else { return { panelBg: '#eeeeee', panelBorder: 'rgba(102, 102, 102, 0.3)', statBoxBg: '#ffffff', statBoxBorder: 'rgba(102, 102, 102, 0.3)', statBoxShadow: 'rgba(50, 50, 50, 0.2) 0px 0px 2px 0px', textPrimary: '#212529', textSecondary: '#212529', textMuted: '#666', success: 'rgb(105, 168, 41)', danger: '#dc3545', primary: '#0092d8', configBg: '#ffffff', configBorder: '#ced4da', inputBg: '#ffffff', inputBorder: '#ced4da' }; } } // Format number with commas function formatNumberWithCommas(num) { if (num === 0) return '0'; return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } // Parse number from comma-formatted string function parseCommaNumber(str) { return parseInt(str.replace(/,/g, '')) || 0; } // Detect ranking outcome from faction name for winner/loser determination function detectRankingOutcome(factionName) { if (!factionName || typeof factionName !== 'string') { return null; // Cannot determine } const lowerName = factionName.toLowerCase(); if (lowerName.includes('ranked down')) { return 'loser'; } else if (lowerName.includes('ranked up') || lowerName.includes('remained at')) { return 'winner'; } return null; // Cannot determine } // Create dismissible info box for when no pricing is available function createNoPricingInfoBox() { const colors = getThemeColors(); const mobile = isMobile(); const infoBox = document.createElement('div'); infoBox.id = 'rw-no-pricing-info'; infoBox.style.cssText = ` background: ${colors.statBoxBg}; border: 1px solid ${colors.primary}; border-radius: 6px; padding: ${mobile ? '12px' : '16px'}; margin: 15px 0; position: relative; box-shadow: ${colors.statBoxShadow}; `; // Close button (X) const closeButton = document.createElement('span'); closeButton.style.cssText = ` position: absolute; top: 8px; right: 12px; font-size: 18px; font-weight: bold; color: ${colors.textMuted}; cursor: pointer; user-select: none; line-height: 1; `; closeButton.innerHTML = mobile ? '×' : '×'; closeButton.title = 'Dismiss this message'; // Close functionality closeButton.addEventListener('click', function() { infoBox.remove(); // Remember dismissal if (typeof(Storage) !== "undefined") { localStorage.setItem('rw_no_pricing_info_dismissed', 'true'); } }); // Info content const content = document.createElement('div'); content.style.cssText = ` color: ${colors.textPrimary}; font-size: ${mobile ? '11px' : '12px'}; line-height: 1.4; `; // Title const title = document.createElement('div'); title.style.cssText = ` font-weight: bold; margin-bottom: 8px; color: ${colors.primary}; font-size: ${mobile ? '13px' : '14px'}; `; title.textContent = 'No Pricing Configured'; // Message content container const message = document.createElement('div'); // Description text const description = document.createElement('div'); description.style.marginBottom = '8px'; description.textContent = 'Cache reward values are not being calculated as no pricing sources are configured.'; // "To see reward values:" text const instructionHeader = document.createElement('div'); instructionHeader.style.cssText = `font-weight: bold; margin-bottom: 4px;`; instructionHeader.textContent = 'To see reward values:'; // Bullet points container const bulletPoints = document.createElement('div'); // First bullet point - Price List const bullet1 = document.createElement('div'); bullet1.style.marginBottom = '2px'; bullet1.innerHTML = (mobile ? '• ' : '• ') + 'Configure custom prices using '; const priceListLink = document.createElement('span'); priceListLink.textContent = 'Price List'; priceListLink.style.cssText = ` color: ${colors.primary}; `; bullet1.appendChild(priceListLink); bullet1.innerHTML += ', and/or'; // Second bullet point - Settings const bullet2 = document.createElement('div'); bullet2.innerHTML = (mobile ? '• ' : '• ') + 'Enable market values in '; const settingsLink = document.createElement('span'); settingsLink.textContent = 'Settings'; settingsLink.style.cssText = ` color: ${colors.primary}; `; bullet2.appendChild(settingsLink); bullet2.innerHTML += ' (Public API key)'; bulletPoints.appendChild(bullet1); bulletPoints.appendChild(bullet2); message.appendChild(description); message.appendChild(instructionHeader); message.appendChild(bulletPoints); content.appendChild(title); content.appendChild(message); infoBox.appendChild(closeButton); infoBox.appendChild(content); return infoBox; } // Check if no pricing info box should be shown function shouldShowNoPricingInfo() { // Don't show if user has dismissed it if (typeof(Storage) !== "undefined") { const dismissed = localStorage.getItem('rw_no_pricing_info_dismissed'); if (dismissed === 'true') { return false; } } // Show if no valid API key AND no custom prices configured const hasValidApiKey = (API_KEY && API_KEY !== 'YOUR_API_KEY_HERE') || PDA_VALIDATED; const hasCustomPrices = SETTINGS.showCustomPrices && sellerData.sellers.some(seller => Object.values(seller.prices).some(price => price > 0) ); // Hide info box if user has either a valid API key OR configured custom prices return !hasValidApiKey && !hasCustomPrices; } // ======================================== // SETTINGS & DATA PERSISTENCE // ======================================== // Load settings from browser storage function loadSettings() { if (typeof(Storage) !== "undefined") { const saved = localStorage.getItem('rw_cache_settings'); if (saved) { try { SETTINGS = Object.assign(SETTINGS, JSON.parse(saved)); } catch (e) { console.log('RW: Could not load settings'); } } const savedKey = localStorage.getItem('rw_api_key'); if (savedKey) { API_KEY = savedKey; } const savedUsername = localStorage.getItem('rw_api_username'); if (savedUsername) { API_USERNAME = savedUsername; console.log('RW: Loaded username from localStorage:', API_USERNAME); } const savedPdaValidated = localStorage.getItem('rw_pda_validated'); if (savedPdaValidated === 'true') { PDA_VALIDATED = true; console.log('RW: PDA State Transition - Loaded validated state from localStorage'); console.log('RW: PDA State - Mode:', PDA_MODE, 'Validated:', PDA_VALIDATED, 'Username:', API_USERNAME); } else { console.log('RW: PDA State - No validation flag in localStorage, PDA_MODE:', PDA_MODE); } // Load API price cache from localStorage const savedApiCache = localStorage.getItem('rw_api_price_cache'); if (savedApiCache) { try { apiPriceCache = JSON.parse(savedApiCache); console.log('RW: Loaded API price cache from localStorage:', apiPriceCache); } catch (e) { console.log('RW: Could not load API price cache from localStorage'); } } // Load seller data with migration support const savedSellers = localStorage.getItem('rw_seller_data'); if (savedSellers) { try { const parsed = JSON.parse(savedSellers); if (parsed && parsed.sellers && Array.isArray(parsed.sellers)) { sellerData = parsed; console.log('RW: Loaded seller data:', sellerData); } } catch (e) { console.log('RW: Could not load seller data, using defaults'); } } } } // Save settings to localStorage function saveSettings() { if (typeof(Storage) !== "undefined") { localStorage.setItem('rw_cache_settings', JSON.stringify(SETTINGS)); if (API_KEY !== 'YOUR_API_KEY_HERE') { localStorage.setItem('rw_api_key', API_KEY); } if (API_USERNAME) { localStorage.setItem('rw_api_username', API_USERNAME); console.log('RW: Saved username to localStorage:', API_USERNAME); } if (PDA_VALIDATED) { localStorage.setItem('rw_pda_validated', 'true'); console.log('RW: PDA State Transition - Saved validated state to localStorage'); console.log('RW: PDA State - Mode:', PDA_MODE, 'Validated:', PDA_VALIDATED, 'Username:', API_USERNAME); } else if (PDA_MODE) { // Clear the flag if PDA mode but not validated localStorage.removeItem('rw_pda_validated'); console.log('RW: PDA State Transition - Cleared validation flag from localStorage'); } // Save seller data localStorage.setItem('rw_seller_data', JSON.stringify(sellerData)); console.log('RW: Saved seller data to localStorage'); // Clear no-pricing info dismissal if pricing is now configured const hasApiValues = SETTINGS.showApiValues && apiPriceCache.data && Object.keys(apiPriceCache.data).length > 0; const hasCustomPrices = SETTINGS.showCustomPrices && sellerData.sellers.some(seller => Object.values(seller.prices).some(price => price > 0) ); if (hasApiValues || hasCustomPrices) { localStorage.removeItem('rw_no_pricing_info_dismissed'); } } } // PDA reconnection logic - attempt to restore lost validation state async function attemptPdaReconnection() { if (!PDA_MODE) return; // If we have a username but no validation flag, try to reconnect if (API_USERNAME && !PDA_VALIDATED) { console.log('RW: PDA Reconnection - Attempting to restore validation for user:', API_USERNAME); try { const testResult = await testApiKey(''); if (testResult.success && testResult.isPDA && testResult.name === API_USERNAME) { console.log('RW: PDA Reconnection - Successfully restored validation'); PDA_VALIDATED = true; saveSettings(); } else { console.log('RW: PDA Reconnection - Failed to restore validation'); } } catch (error) { console.log('RW: PDA Reconnection - Error during reconnection attempt:', error); } } } // Initialize settings - LOAD FIRST BEFORE ANYTHING ELSE loadSettings(); console.log('RW: Loaded settings - Active seller:', sellerData.activeSeller, 'Seller name:', sellerData.sellers[sellerData.activeSeller].name); // Attempt PDA reconnection if needed if (PDA_MODE && API_USERNAME && !PDA_VALIDATED) { console.log('RW: Detected potential PDA reconnection scenario'); attemptPdaReconnection(); } // Get current seller's custom price for an item function getCustomPrice(cacheType) { const seller = sellerData.sellers[sellerData.activeSeller]; const priceValue = seller.prices[cacheType] || 0; if (priceValue === 0) return 0; // Handle relative pricing mode if (seller.pricingMode === 'relative') { // Calculate actual price from percentage of API price if (apiPriceCache.data && apiPriceCache.data[cacheType]) { const apiPrice = apiPriceCache.data[cacheType]; const calculatedPrice = Math.round(apiPrice * (priceValue / 100)); console.log(`RW: Relative pricing - ${cacheType}: ${priceValue}% of ${apiPrice} = ${calculatedPrice}`); return calculatedPrice; } else { // No API price available for calculation console.log(`RW: Relative pricing - ${cacheType}: No API price available for ${priceValue}%`); return 0; } } else { // Fixed pricing mode - return as-is return priceValue; } } // Check if API cache needs refreshing based on GMT timing function shouldRefreshApiCache() { if (!apiPriceCache.lastFetched) return true; // No cache const lastFetch = new Date(apiPriceCache.lastFetched); const now = new Date(); // Convert both to UTC for GMT comparison const lastFetchUTC = new Date(lastFetch.getUTCFullYear(), lastFetch.getUTCMonth(), lastFetch.getUTCDate(), lastFetch.getUTCHours()); const nowUTC = new Date(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours()); // Different dates = definitely refresh if (lastFetchUTC.getUTCDate() !== nowUTC.getUTCDate() || lastFetchUTC.getUTCMonth() !== nowUTC.getUTCMonth() || lastFetchUTC.getUTCFullYear() !== nowUTC.getUTCFullYear()) { return true; } // Same date: check 3am GMT rule const lastHour = lastFetchUTC.getUTCHours(); const nowHour = nowUTC.getUTCHours(); return (lastHour < 3 && nowHour >= 3); // Was before 3am GMT, now after } // Function to update API price cache async function updateApiPriceCache(forceRefresh = false) { const currentApiKey = getApiKey(); if (!currentApiKey || currentApiKey === 'YOUR_API_KEY_HERE') return false; // Check if we need to refresh the cache (skip check if forced) if (!forceRefresh && !shouldRefreshApiCache()) { console.log('RW: Using cached API prices (still fresh)'); return true; // We have fresh data } console.log('RW: Cache expired or missing, fetching fresh API prices...'); try { const result = await fetchApiPrices(); if (result.success) { // Store with timestamp apiPriceCache = { lastFetched: new Date().toISOString(), data: result.prices }; // Save to localStorage for persistence across page refreshes if (typeof(Storage) !== "undefined") { localStorage.setItem('rw_api_price_cache', JSON.stringify(apiPriceCache)); console.log('RW: Saved API price cache to localStorage'); } console.log('RW: Updated API price cache:', apiPriceCache); return true; } else { console.warn('RW: Failed to fetch API prices:', result.error); return false; } } catch (error) { console.error('RW: Error updating API cache:', error); return false; } } // ======================================== // API INTEGRATION & REQUESTS // ======================================== // Handle API requests - works with both browser and PDA mobile app function makeApiRequest(url, options = {}) { return new Promise(async function(resolve) { const timeout = options.timeout || 10000; if (PDA_MODE && typeof window.PDA_httpGet === 'function') { if (DEBUG_MODE) console.log('RW: Using PDA API request for:', url); console.log('RW: Using PDA API request'); try { // Set up timeout for PDA requests const timeoutId = setTimeout(function() { console.error('RW: PDA API request timeout'); resolve({ success: false, error: 'PDA API request timed out. Please check your connection and try again.' }); }, timeout); // PDA_httpGet returns a Promise, not a callback const response = await window.PDA_httpGet(url); clearTimeout(timeoutId); console.log('RW: PDA API response received:', response); try { let data; if (typeof response.responseText === 'string') { data = JSON.parse(response.responseText); } else if (typeof response.responseText === 'object' && response.responseText !== null) { data = response.responseText; } else { console.error('RW: Unexpected PDA response format:', response); resolve({ success: false, error: 'PDA API response format error. Please ensure your API key is connected to PDA.' }); return; } console.log('RW: PDA API parsed data:', data); resolve({ success: true, data: data }); } catch (error) { console.error('RW: PDA API response parse error:', error); resolve({ success: false, error: 'PDA API response could not be parsed. Please check your PDA API connection.' }); } } catch (error) { console.error('RW: PDA API request error:', error); resolve({ success: false, error: 'PDA API request failed. Please ensure your API key is properly connected to PDA and try again.' }); } } else { // Check if GM.xmlHttpRequest is available, otherwise use fetch if (typeof GM !== 'undefined' && typeof GM.xmlHttpRequest === 'function') { if (DEBUG_MODE) console.log('RW: Using GM API request for:', url); console.log('RW: Using GM API request'); GM.xmlHttpRequest({ method: 'GET', url: url, timeout: timeout, onload: function(response) { try { const data = JSON.parse(response.responseText); resolve({ success: true, data: data }); } catch (error) { resolve({ success: false, error: 'Invalid response from API' }); } }, onerror: function() { resolve({ success: false, error: 'Network error' }); }, ontimeout: function() { resolve({ success: false, error: 'Request timed out' }); } }); } else { // Fallback to fetch API console.log('RW: GM not available, using fetch API'); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); fetch(url, { signal: controller.signal, method: 'GET' }) .then(response => { clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response.json(); }) .then(data => { resolve({ success: true, data: data }); }) .catch(error => { clearTimeout(timeoutId); if (error.name === 'AbortError') { resolve({ success: false, error: 'Request timed out' }); } else { resolve({ success: false, error: error.message || 'Network error' }); } }); } } }); } // Get the appropriate API key for requests function getApiKey() { if (PDA_MODE && (!API_KEY || API_KEY === 'YOUR_API_KEY_HERE')) { return '###PDA-APIKEY###'; // Direct placeholder for PDA replacement } return API_KEY; } async function testApiKey(apiKey) { return new Promise(async function(resolve) { // Handle PDA mode with empty API key if (PDA_MODE && (!apiKey || apiKey === 'YOUR_API_KEY_HERE')) { console.log('RW: Testing PDA API key'); // Use the complete URL constant that PDA should have processed const url = PDA_USER_API_URL; if (DEBUG_MODE) console.log('RW: PDA API URL (key should be replaced):', url); try { const result = await makeApiRequest(url, { timeout: 15000 }); console.log('RW: PDA API test result:', result); if (result.success) { if (result.data.error) { console.log('RW: PDA API returned error:', result.data.error); let errorMsg = result.data.error.error; if (errorMsg.includes('Incorrect API key')) { errorMsg = 'API key not connected to PDA. Please connect your API key in PDA settings first.'; } resolve({ success: false, error: errorMsg }); } else { console.log('RW: PDA API test successful:', result.data.name); resolve({ success: true, name: result.data.name, isPDA: true }); } } else { console.log('RW: PDA API request failed:', result.error); resolve({ success: false, error: 'PDA connection failed: ' + result.error }); } } catch (error) { console.error('RW: PDA API test exception:', error); resolve({ success: false, error: 'PDA API validation failed. Please ensure PDA is properly configured with your API key.' }); } return; } // Standard API key validation if (!apiKey || apiKey === 'YOUR_API_KEY_HERE') { resolve({ success: false, error: 'No API key provided' }); return; } const url = 'https://api.torn.com/user/?selections=basic&key=' + apiKey; const result = await makeApiRequest(url, { timeout: 10000 }); if (result.success) { if (result.data.error) { resolve({ success: false, error: result.data.error.error }); } else { resolve({ success: true, name: result.data.name, isPDA: false }); } } else { resolve({ success: false, error: result.error }); } }); } async function fetchApiPrices() { return new Promise(async function(resolve) { let currentApiKey; if (PDA_MODE && (!API_KEY || API_KEY === 'YOUR_API_KEY_HERE')) { currentApiKey = '###PDA-APIKEY###'; // Direct placeholder for PDA } else { currentApiKey = API_KEY; } if (!currentApiKey || currentApiKey === 'YOUR_API_KEY_HERE') { resolve({ success: false, error: 'No API key configured' }); return; } // Correct cache item IDs const cacheItems = { armorCache: '1118', // Armor Cache meleeCache: '1119', // Melee Cache smallArmsCache: '1120', // Small Arms Cache mediumArmsCache: '1121', // Medium Arms Cache heavyArmsCache: '1122' // Heavy Arms Cache }; const url = 'https://api.torn.com/torn/?selections=items&key=' + currentApiKey; const result = await makeApiRequest(url, { timeout: 15000 }); if (result.success) { if (result.data.error) { resolve({ success: false, error: result.data.error.error }); } else { const prices = {}; Object.keys(cacheItems).forEach(function(key) { const itemId = cacheItems[key]; const item = result.data.items[itemId]; if (item && item.market_value) { prices[key] = item.market_value; } }); console.log('RW: Cache prices from API:', prices); resolve({ success: true, prices: prices }); } } else { resolve({ success: false, error: result.error }); } }); } async function fetchPointValue() { return new Promise(async function(resolve) { let currentApiKey; if (PDA_MODE && (!API_KEY || API_KEY === 'YOUR_API_KEY_HERE')) { currentApiKey = '###PDA-APIKEY###'; // Direct placeholder for PDA } else { currentApiKey = API_KEY; } if (!currentApiKey || currentApiKey === 'YOUR_API_KEY_HERE') { resolve(31300); // Default mock value return; } const url = 'https://api.torn.com/torn/?selections=pointsmarket&key=' + currentApiKey; const result = await makeApiRequest(url, { timeout: 10000 }); if (result.success) { if (result.data.error) { console.warn('RW: Could not fetch point value:', result.data.error.error); resolve(31300); // Fallback } else { const pointValue = result.data.pointsmarket ? result.data.pointsmarket.cost : 31300; resolve(pointValue); } } else { console.warn('RW: Point value API error:', result.error); resolve(31300); // Fallback } }); } function numberFormatter(num, digits) { digits = digits || 1; const lookup = [ { value: 1, symbol: "" }, { value: 1e3, symbol: "k" }, { value: 1e6, symbol: "M" }, { value: 1e9, symbol: "B" }, ]; const rx = /\.0+$|(\.[0-9]*[1-9])0+$/; var item = lookup .slice() .reverse() .find(function (item) { return num >= item.value; }); return item ? (num / item.value).toFixed(digits).replace(rx, "$1") + item.symbol : "0"; } // Helper function to extract user ID from seller name function extractUserInfo(sellerName) { const match = sellerName.match(/^(.+?)\s*\[(\d+)\]$/); if (match) { return { name: match[1].trim(), id: match[2], hasId: true }; } return { name: sellerName.trim(), id: null, hasId: false }; } // Helper function to create profile link function createProfileLink(userId, colors) { const link = document.createElement('a'); link.href = 'https://www.torn.com/profiles.php?XID=' + userId; link.target = '_blank'; link.style.cssText = 'margin-left: 4px; color: ' + colors.primary + '; text-decoration: none; font-size: 12px;'; // Use HTML entity for mobile compatibility if (isMobile()) { link.innerHTML = '🔗'; } else { link.innerHTML = '🔗'; } link.title = 'View profile [' + userId + ']'; return link; } // ======================================== // MAIN UI CREATION & DISPLAY // ======================================== // Add control buttons to the page header (run only once) function addIconsToTopHeader() { const colors = getThemeColors(); // Find the "Ranked War # header const titleHeader = document.querySelector('.title-black.m-top10.top-round') || document.querySelector('.title-black') || document.querySelector('[class*="title"]'); if (!titleHeader) { console.log("RWAwardValue: Could not find title header for icons"); return; } // Check if we already added buttons to avoid duplicates if (titleHeader.querySelector('#rw-price-btn-header')) { console.log("RWAwardValue: Header buttons already exist, skipping"); return; } console.log("RWAwardValue: Adding icons to top header"); // Make the header a flex container with proper alignment titleHeader.style.display = 'flex'; titleHeader.style.justifyContent = 'space-between'; titleHeader.style.alignItems = 'center'; titleHeader.style.paddingLeft = titleHeader.style.paddingLeft || '15px'; titleHeader.style.paddingRight = '15px'; titleHeader.style.boxSizing = 'border-box'; // Create button container with proper vertical alignment const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = 'display: flex; align-items: center; height: 100%; margin-left: auto;'; // Price list button with text instead of icon const priceButton = document.createElement('button'); priceButton.id = 'rw-price-btn-header'; priceButton.textContent = 'Price List'; priceButton.title = 'Manage Cache Prices'; priceButton.style.cssText = 'background: transparent; color: rgba(255, 255, 255, 0.8); border: none; padding: 8px 12px; border-radius: 3px; cursor: pointer; font-size: 1em; font-weight: normal; transition: color 0.2s ease; line-height: 1; min-height: 32px;'; // Separator const separator = document.createElement('span'); separator.textContent = ' | '; separator.style.cssText = 'color: rgba(255, 255, 255, 0.5); font-size: 1em; margin: 0 4px;'; // Settings button with text instead of icon const settingsButton = document.createElement('button'); settingsButton.id = 'rw-settings-btn-header'; settingsButton.textContent = 'Settings'; settingsButton.title = 'RW Script Settings'; settingsButton.style.cssText = 'background: transparent; color: rgba(255, 255, 255, 0.8); border: none; padding: 8px 12px; border-radius: 3px; cursor: pointer; font-size: 1em; font-weight: normal; transition: color 0.2s ease; line-height: 1; min-height: 32px;'; // Add hover effects priceButton.addEventListener('mouseenter', function() { priceButton.style.color = 'rgba(255, 255, 255, 1)'; }); priceButton.addEventListener('mouseleave', function() { priceButton.style.color = 'rgba(255, 255, 255, 0.8)'; }); settingsButton.addEventListener('mouseenter', function() { settingsButton.style.color = 'rgba(255, 255, 255, 1)'; }); settingsButton.addEventListener('mouseleave', function() { settingsButton.style.color = 'rgba(255, 255, 255, 0.8)'; }); // Add event listeners for the actual panel functionality priceButton.addEventListener('click', function() { console.log('Header price management clicked'); showPricePanel(); }); settingsButton.addEventListener('click', function() { console.log('Header settings clicked'); showSettingsPanel(); }); buttonContainer.appendChild(priceButton); buttonContainer.appendChild(separator); buttonContainer.appendChild(settingsButton); titleHeader.appendChild(buttonContainer); } function refreshRewardDisplay() { // If we have stored raw data, recalculate with current seller prices AND API values if (rawRewardData && rawRewardData.length === 2) { if (DEBUG_MODE) console.log("RWAwardValue: Recalculating with stored data, current seller prices, and API values"); console.log("RWAwardValue: Using seller:", sellerData.sellers[sellerData.activeSeller].name); console.log("RWAwardValue: API cache available:", Object.keys(apiPriceCache).length > 0); // Recalculate totals with current seller prices AND API comparisons rawRewardData.forEach(function(rawReward, index) { let newTotalValue = 0; let apiTotalValue = 0; // Track API total for comparison rawReward.items.forEach(function(item) { let itemValue = 0; let apiValue = 0; if (item.type === 'cache') { const customPrice = getCustomPrice(item.cacheType); const hasCustomPrice = customPrice > 0; const hasApiPrice = apiPriceCache.data && apiPriceCache.data[item.cacheType]; const hasValidApiKey = (API_KEY && API_KEY !== 'YOUR_API_KEY_HERE') || PDA_VALIDATED; let baseValue = 0; // Use same logic as initial calculation if (SETTINGS.showCustomPrices && hasCustomPrice) { baseValue = customPrice; } else if (hasApiPrice && hasValidApiKey) { baseValue = apiPriceCache.data[item.cacheType]; } else { baseValue = 0; // Show "?" instead of old defaults } itemValue = baseValue * item.quantity; console.log('RWAwardValue: ' + item.cacheType + ' - custom: ' + customPrice + ', using: ' + baseValue + ', quantity: ' + item.quantity + ', total: ' + itemValue); // Get API value if available if (SETTINGS.showApiValues && apiPriceCache.data[item.cacheType]) { apiValue = apiPriceCache.data[item.cacheType] * item.quantity; apiTotalValue += apiValue; if (DEBUG_MODE) console.log('RWAwardValue: ' + item.cacheType + ' API: ' + apiPriceCache.data[item.cacheType] + ' * ' + item.quantity + ' = ' + apiValue); } // Store individual item calculations for dropdown details item.calculatedValue = itemValue; item.calculatedApiValue = apiValue; } else if (item.type === 'points') { itemValue = item.pointValue * item.quantity; apiValue = itemValue; // Points are the same for both apiTotalValue += apiValue; item.calculatedValue = itemValue; item.calculatedApiValue = apiValue; } newTotalValue += itemValue; }); // Update the stored reward data WITH API totals rewardData[index].totalValue = newTotalValue; rewardData[index].apiTotalValue = apiTotalValue; // Store API total for display if (DEBUG_MODE) console.log('RWAwardValue: ' + rawReward.factionName + ' new total: ' + newTotalValue + ', API total: ' + apiTotalValue); }); } // Clear existing containers and recreate with updated data const existingContainer = document.getElementById('rw-panels-container'); if (existingContainer) { existingContainer.remove(); } // Recreate with updated data (includes API values in headers) setTimeout(function() { createAndDisplayContainers(); }, 100); } function createAndDisplayContainers() { console.log("RWAwardValue: Creating containers..."); if (rewardData.length !== 2) { console.error("RWAwardValue: Expected 2 rewards, got", rewardData.length); return; } // Check if containers already exist to prevent duplicates const existingContainer = document.getElementById('rw-panels-container'); if (existingContainer) { console.log("RWAwardValue: Panels already exist, removing old ones first"); existingContainer.remove(); } const grandTotalValue = rewardData[0].totalValue + rewardData[1].totalValue; console.log("RWAwardValue: Grand total value:", grandTotalValue); // First, add icons to the top header addIconsToTopHeader(); // Find the insertion point - target right after the war declaration section const reportTitle = document.querySelector('.report-title'); const firstFactionText = document.querySelector('ul'); let insertionPoint = null; let insertMethod = 'fallback'; if (reportTitle && firstFactionText) { // Find the element that contains both the title and the faction descriptions const titleParent = reportTitle.parentElement; const factionParent = firstFactionText.parentElement; if (titleParent === factionParent) { // Same container - insert between them insertionPoint = titleParent; insertMethod = 'insertBefore'; console.log("RWAwardValue: Will insert between title and faction descriptions"); } else { // Different containers - insert after title insertionPoint = reportTitle; insertMethod = 'afterTitle'; console.log("RWAwardValue: Will insert right after report title"); } } else { // Fallback insertionPoint = reportTitle || document.body; insertMethod = 'fallback'; console.log("RWAwardValue: Using fallback insertion"); } // Create container for all our panels with proper spacing const panelContainer = document.createElement('div'); panelContainer.id = 'rw-panels-container'; panelContainer.style.cssText = 'margin: 15px 10px 20px 10px; padding: 0; display: block; visibility: visible; opacity: 1; position: relative; z-index: 1;'; console.log("RWAwardValue: Created panel container with proper spacing"); // Add trader selector first (above all containers) - only if custom prices are enabled if (SETTINGS.showCustomPrices) { const traderSelector = createTraderSelector(); if (traderSelector) { panelContainer.appendChild(traderSelector); } } // Add info box if no pricing sources are configured if (shouldShowNoPricingInfo()) { const infoBox = createNoPricingInfoBox(); panelContainer.appendChild(infoBox); } // Create individual reward containers for (let i = 0; i < 2; i++) { console.log("RWAwardValue: Creating container for reward", i); const container = createCompactContainer( rewardData[i].totalValue, i, grandTotalValue, rewardData[i].factionName, rewardData // Pass all faction data for ranking analysis ); // Add click listener for expansion const header = container.querySelector('#rw-reward-' + i); header.addEventListener('click', function() { toggleExpansion(i); }); // Populate details with updated item elements const details = container.querySelector('#rw-details-' + i); // Use the updated item elements that include API values if (rawRewardData && rawRewardData[i] && rawRewardData[i].items) { console.log('RWAwardValue: Building updated item elements for', rewardData[i].factionName); // Clear existing items details.innerHTML = ''; // Pre-calculate colors and mobile flag to avoid unsafe references const detailColors = getThemeColors(); const isMobileView = isMobile(); // Create fresh item elements with current calculations rawRewardData[i].items.forEach(function(item) { const itemDiv = document.createElement('div'); itemDiv.style.background = detailColors.statBoxBg; itemDiv.style.padding = isMobileView ? '8px' : '10px'; itemDiv.style.borderRadius = '3px'; itemDiv.style.marginBottom = '6px'; itemDiv.style.borderLeft = '3px solid ' + detailColors.primary; itemDiv.style.border = '1px solid ' + detailColors.statBoxBorder; itemDiv.style.boxShadow = detailColors.statBoxShadow; itemDiv.style.display = 'flex'; itemDiv.style.justifyContent = 'space-between'; itemDiv.style.alignItems = 'center'; itemDiv.style.fontSize = isMobileView ? '12px' : '13px'; const nameSpan = document.createElement('span'); nameSpan.style.color = detailColors.textSecondary; // Create enhanced item name format let itemName = ''; if (item.type === 'cache') { // Use proper cache type names let cacheTypeName = ''; switch(item.cacheType) { case 'armorCache': cacheTypeName = 'Armor'; break; case 'heavyArmsCache': cacheTypeName = 'Heavy Arms'; break; case 'mediumArmsCache': cacheTypeName = 'Medium Arms'; break; case 'meleeCache': cacheTypeName = 'Melee'; break; case 'smallArmsCache': cacheTypeName = 'Small Arms'; break; } if (isMobileView) { // Mobile: Simplified layout without quantity breakdown if (item.quantity === 1) { itemName = '1x ' + cacheTypeName + ' Cache'; } else { itemName = item.quantity + 'x ' + cacheTypeName + ' Cache'; } // Add API comparison on second line if available if (SETTINGS.showApiValues && item.calculatedApiValue > 0) { const apiValue = item.calculatedApiValue; const customValue = item.calculatedValue; const percentDiff = customValue > 0 ? ((customValue - apiValue) / apiValue * 100) : 0; if (Math.abs(percentDiff) > 0.1) { let arrow = ''; let arrowColor = detailColors.textMuted; if (percentDiff > 0) { arrow = ' ' + getMobileArrow(true) + ' '; arrowColor = detailColors.success; } else { arrow = ' ' + getMobileArrow(false) + ' '; arrowColor = detailColors.danger; } itemName += '<br><span style="font-size: 11px; color: ' + detailColors.textMuted + ';"><span style="color: ' + arrowColor + ';">' + arrow + Math.abs(percentDiff).toFixed(1) + '%</span> Market value ' + numberFormatter(apiValue, 2) + '</span>'; } } } else { // Desktop: Single line layout (unchanged) const individualPrice = item.calculatedValue / item.quantity; if (item.quantity === 1) { if (individualPrice > 0 && !isNaN(individualPrice)) { itemName = '1x ' + cacheTypeName + ' Cache (' + formatNumberWithCommas(individualPrice) + ')'; } else { itemName = '1x ' + cacheTypeName + ' Cache'; } } else { if (individualPrice > 0 && !isNaN(individualPrice)) { itemName = item.quantity + 'x ' + cacheTypeName + ' Cache (' + item.quantity + 'x ' + formatNumberWithCommas(individualPrice) + ')'; } else { itemName = item.quantity + 'x ' + cacheTypeName + ' Cache'; } } } } else if (item.type === 'points') { if (isMobileView) { // Mobile: Keep points simple itemName = item.quantity.toLocaleString() + ' points<br><span style="font-size: 11px; color: ' + detailColors.textMuted + ';">(' + (item.calculatedValue > 0 ? numberFormatter(item.calculatedValue) : '?') + ' total)</span>'; } else { // Desktop: Single line itemName = item.quantity.toLocaleString() + ' points (' + (item.calculatedValue > 0 ? numberFormatter(item.calculatedValue) : '?') + ' total)'; } } nameSpan.innerHTML = itemName; // Use innerHTML for mobile line breaks const valueContainer = document.createElement('div'); valueContainer.style.display = 'flex'; valueContainer.style.alignItems = 'center'; valueContainer.style.gap = '8px'; // Add API comparison if enabled and available - DESKTOP ONLY (mobile has it inline) if (!isMobileView && SETTINGS.showApiValues && item.calculatedApiValue > 0 && item.type === 'cache') { const apiValue = item.calculatedApiValue; const customValue = item.calculatedValue; const percentDiff = customValue > 0 ? ((customValue - apiValue) / apiValue * 100) : 0; console.log('RWAwardValue: Item', item.cacheType, '- Custom:', customValue, 'API:', apiValue, 'Diff:', percentDiff.toFixed(1) + '%'); if (Math.abs(percentDiff) > 0.1) { // Create wrapper for name with API line below const nameWrapper = document.createElement('div'); nameWrapper.style.cssText = 'display: flex; flex-direction: column; align-items: flex-start;'; // Create main item name span const mainNameSpan = document.createElement('span'); mainNameSpan.style.color = detailColors.textSecondary; mainNameSpan.innerHTML = itemName; // Create API comparison line with increased gap const apiLine = document.createElement('div'); apiLine.style.cssText = 'margin-top: 4px; font-size: 11px; color: ' + detailColors.textMuted + '; font-weight: normal;'; let arrow = ''; let arrowColor = detailColors.textMuted; if (percentDiff > 0) { arrow = ' ' + getMobileArrow(true) + ' '; arrowColor = detailColors.success; } else { arrow = ' ' + getMobileArrow(false) + ' '; arrowColor = detailColors.danger; } apiLine.innerHTML = '<span style="color: ' + arrowColor + ';">' + arrow + Math.abs(percentDiff).toFixed(1) + '%</span> Market value ' + numberFormatter(apiValue, 2); nameWrapper.appendChild(mainNameSpan); nameWrapper.appendChild(apiLine); // Adjust itemDiv to allow for multi-line content itemDiv.style.alignItems = 'flex-start'; itemDiv.appendChild(nameWrapper); } else { // No significant API difference, use normal layout nameSpan.innerHTML = itemName; itemDiv.appendChild(nameSpan); } } else { // No API comparison or mobile view, use normal layout nameSpan.innerHTML = itemName; itemDiv.appendChild(nameSpan); } const valueSpan = document.createElement('span'); valueSpan.style.color = detailColors.primary; valueSpan.style.fontWeight = 'bold'; valueSpan.textContent = item.calculatedValue > 0 ? numberFormatter(item.calculatedValue, 2) : '?'; valueContainer.appendChild(valueSpan); // Only add valueContainer if we haven't already added nameWrapper if (!itemDiv.children.length) { itemDiv.appendChild(nameSpan); } itemDiv.appendChild(valueContainer); details.appendChild(itemDiv); }); } else { // Fallback to original method rewardData[i].itemElements.forEach(function(element) { details.appendChild(element); }); } panelContainer.appendChild(container); rewardData[i].container = container; console.log("RWAwardValue: Container", i, "added to panel container"); } // Add grand total to panel container const grandTotalContainer = createGrandTotalContainer(grandTotalValue); panelContainer.appendChild(grandTotalContainer); // Insert the panel container using the appropriate method if (insertMethod === 'insertBefore' && firstFactionText) { // Insert before faction descriptions insertionPoint.insertBefore(panelContainer, firstFactionText); console.log("RWAwardValue: Panels inserted before faction descriptions"); } else if (insertMethod === 'afterTitle' && reportTitle) { // Insert immediately after report title insertionPoint.insertAdjacentElement('afterend', panelContainer); console.log("RWAwardValue: Panels inserted immediately after report title"); } else { // Fallback - insert after title or at top insertionPoint.insertAdjacentElement('afterend', panelContainer); console.log("RWAwardValue: Panels inserted at fallback position"); } console.log("RWAwardValue: All panels inserted at top position"); // Debug: Check if panels are actually in the DOM setTimeout(function() { const insertedContainer = document.getElementById('rw-panels-container'); if (insertedContainer) { console.log("RWAwardValue: Panel container found in DOM"); console.log("RWAwardValue: Container has", insertedContainer.children.length, "children"); console.log("RWAwardValue: Container computed style:", getComputedStyle(insertedContainer).display); // Force visibility if needed insertedContainer.style.display = 'block'; insertedContainer.style.visibility = 'visible'; // Check individual panels for (let i = 0; i < insertedContainer.children.length; i++) { const child = insertedContainer.children[i]; console.log("RWAwardValue: Child", i, "display:", getComputedStyle(child).display); child.style.display = 'block'; child.style.visibility = 'visible'; } } else { console.error("RWAwardValue: Panel container NOT found in DOM!"); // Try fallback placement console.log("RWAwardValue: Attempting fallback placement..."); const fallbackTarget = document.querySelector('.report-title'); if (fallbackTarget) { fallbackTarget.parentElement.appendChild(panelContainer); console.log("RWAwardValue: Fallback placement completed"); } } }, 100); console.log("RWAwardValue: Script completed successfully!"); // Initialize API cache if showing API values - ONLY ONCE, NOT IN REFRESH if (SETTINGS.showApiValues && ((API_KEY && API_KEY !== 'YOUR_API_KEY_HERE') || (PDA_MODE && PDA_VALIDATED)) && !window.rwApiInitialized) { console.log('RW: Initializing API price cache on startup...'); window.rwApiInitialized = true; // Prevent multiple initializations updateApiPriceCache().then(function(success) { if (success) { console.log('RW: Startup API fetch successful - refreshing display to integrate API values'); // We need a full refresh to integrate API values properly into the calculations refreshRewardDisplay(); } else { console.log('RW: Startup API fetch failed'); } }); } console.log("RWAwardValue: Script completed successfully!"); } // Panel Management Functions - Convert to slide-down panels (same as my gym script) function showPricePanel() { const colors = getThemeColors(); const mobile = isMobile(); // Close any existing panel const existingPanel = document.getElementById('rw-price-panel'); if (existingPanel) { existingPanel.remove(); return; // Toggle off } // Close settings panel if open const settingsPanel = document.getElementById('rw-settings-panel'); if (settingsPanel) settingsPanel.remove(); // Find the title header to attach panel after const titleHeader = document.querySelector('.title-black.m-top10.top-round') || document.querySelector('.title-black') || document.querySelector('[class*="title"]'); if (!titleHeader) return; // Create slide-down panel const panel = document.createElement('div'); panel.id = 'rw-price-panel'; panel.style.cssText = 'background: ' + colors.configBg + '; border: none; border-radius: 0; padding: 15px; margin: 0; color: ' + colors.textPrimary + '; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; box-shadow: 0 4px 8px rgba(0,0,0,0.15); position: relative; overflow: hidden; max-height: 0; transition: max-height 0.3s ease, padding 0.3s ease;'; // Animate in setTimeout(function() { panel.style.maxHeight = '600px'; // Increased from 500px to accommodate confirmations }, 10); // Create header const header = document.createElement('div'); header.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid ' + colors.configBorder + ';'; const title = document.createElement('h4'); title.textContent = 'Price Configuration'; title.style.cssText = 'margin: 0; color: ' + colors.textPrimary + '; font-size: 18px;'; const closeBtn = document.createElement('button'); // Use HTML entity for mobile compatibility if (isMobile()) { closeBtn.innerHTML = '✖'; } else { closeBtn.textContent = '✕'; } closeBtn.style.cssText = 'background: none; border: none; font-size: 20px; color: ' + colors.textSecondary + '; cursor: pointer; padding: 5px; border-radius: 3px;'; closeBtn.onclick = function() { panel.style.maxHeight = '0'; panel.style.padding = '0 15px'; setTimeout(function() { panel.remove(); // Refresh values when panel closes refreshRewardDisplay(); }, 300); }; header.appendChild(title); header.appendChild(closeBtn); // Create seller section const sellerSection = document.createElement('div'); sellerSection.style.marginBottom = '20px'; const sellerLabel = document.createElement('label'); sellerLabel.textContent = 'Active Trader:'; sellerLabel.style.cssText = 'display: block; margin-bottom: 8px; font-weight: bold; color: ' + colors.textPrimary + ';'; const sellerContainer = document.createElement('div'); sellerContainer.style.cssText = 'display: flex; gap: 10px; align-items: center; margin-bottom: 10px;'; // Create last modified date display const lastModifiedDiv = document.createElement('div'); lastModifiedDiv.style.cssText = 'font-size: 12px; color: ' + colors.textMuted + '; margin-bottom: 15px;'; function formatLastModified(dateString) { if (!dateString) return 'Last modified: Unknown'; const date = new Date(dateString); const now = new Date(); const diffTime = Math.abs(now - date); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; const formattedDate = date.getDate() + ' ' + months[date.getMonth()]; if (diffDays === 1) { return 'Last modified: ' + formattedDate + ' (today)'; } else if (diffDays === 2) { return 'Last modified: ' + formattedDate + ' (yesterday)'; } else { return 'Last modified: ' + formattedDate + ' (' + (diffDays - 1) + ' days ago)'; } } const currentSeller = sellerData.sellers[sellerData.activeSeller]; lastModifiedDiv.textContent = formatLastModified(currentSeller.lastModified); const sellerSelect = document.createElement('select'); sellerSelect.style.cssText = 'background: ' + colors.inputBg + '; border: 1px solid ' + colors.inputBorder + '; color: ' + colors.textPrimary + '; padding: 8px; border-radius: 4px; flex: 1;'; sellerData.sellers.forEach(function(seller, index) { const option = document.createElement('option'); option.value = index; const userInfo = extractUserInfo(seller.name); option.textContent = userInfo.name + (userInfo.hasId ? ' [' + userInfo.id + ']' : ''); option.selected = index === sellerData.activeSeller; sellerSelect.appendChild(option); }); const editSellerBtn = document.createElement('button'); editSellerBtn.textContent = 'Edit Name'; editSellerBtn.style.cssText = 'background: ' + colors.primary + '; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;'; // Add seller name input field (initially hidden) const nameInputContainer = document.createElement('div'); nameInputContainer.style.cssText = 'display: none; margin-top: 10px;'; const nameLabel = document.createElement('label'); nameLabel.textContent = 'Trader Name (e.g. "John Doe [123456]"):'; nameLabel.style.cssText = 'display: block; margin-bottom: 5px; color: ' + colors.textSecondary + '; font-size: 12px;'; const nameInputRow = document.createElement('div'); nameInputRow.style.cssText = 'display: flex; gap: 8px; align-items: center; flex-wrap: ' + (mobile ? 'wrap' : 'nowrap') + ';'; const nameInput = document.createElement('input'); nameInput.type = 'text'; nameInput.placeholder = 'Enter seller name with optional [ID]'; nameInput.style.cssText = 'background: ' + colors.inputBg + '; border: 1px solid ' + colors.inputBorder + '; color: ' + colors.textPrimary + '; padding: 8px; border-radius: 4px; flex: 1; ' + (mobile ? 'width: 100%; margin-bottom: 8px;' : ''); const saveNameBtn = document.createElement('button'); saveNameBtn.textContent = 'Save'; saveNameBtn.style.cssText = 'background: ' + colors.success + '; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; ' + (mobile ? 'flex: 1;' : ''); const cancelNameBtn = document.createElement('button'); cancelNameBtn.textContent = 'Cancel'; cancelNameBtn.style.cssText = 'background: ' + colors.danger + '; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; ' + (mobile ? 'flex: 1;' : ''); nameInputContainer.appendChild(nameLabel); if (mobile) { nameInputRow.appendChild(nameInput); nameInputRow.appendChild(document.createElement('div')); // Line break const buttonRow = document.createElement('div'); buttonRow.style.cssText = 'display: flex; gap: 8px; width: 100%;'; buttonRow.appendChild(saveNameBtn); buttonRow.appendChild(cancelNameBtn); nameInputContainer.appendChild(nameInputRow); nameInputContainer.appendChild(buttonRow); } else { nameInputRow.appendChild(nameInput); nameInputRow.appendChild(saveNameBtn); nameInputRow.appendChild(cancelNameBtn); nameInputContainer.appendChild(nameInputRow); } sellerContainer.appendChild(sellerSelect); sellerContainer.appendChild(editSellerBtn); sellerSection.appendChild(sellerLabel); sellerSection.appendChild(sellerContainer); sellerSection.appendChild(lastModifiedDiv); sellerSection.appendChild(nameInputContainer); // Add CSS for placeholder styling const styleId = 'rw-placeholder-styles'; if (!document.getElementById(styleId)) { const style = document.createElement('style'); style.id = styleId; style.textContent = ` .rw-relative-input::placeholder { color: #999 !important; opacity: 1 !important; font-style: italic !important; } .rw-relative-input::-webkit-input-placeholder { color: #999 !important; opacity: 1 !important; font-style: italic !important; } .rw-relative-input::-moz-placeholder { color: #999 !important; opacity: 1 !important; font-style: italic !important; } `; document.head.appendChild(style); } // Create pricing mode toggle const pricingModeSection = document.createElement('div'); pricingModeSection.style.cssText = 'margin-bottom: 20px;'; const pricingModeLabel = document.createElement('label'); pricingModeLabel.textContent = 'Pricing Mode:'; pricingModeLabel.style.cssText = 'display: block; margin-bottom: 8px; font-weight: bold; color: ' + colors.textPrimary + ';'; // Create horizontal toggle const toggleContainer = document.createElement('div'); const toggleBg = currentTheme === 'light' ? '#e8e8e8' : colors.inputBg; toggleContainer.style.cssText = ` display: flex; background: ${toggleBg}; border: 1px solid ${colors.inputBorder}; border-radius: 6px; overflow: hidden; width: 100%; `; const isRelativeMode = currentSeller.pricingMode === 'relative'; // Fixed prices option const fixedOption = document.createElement('div'); fixedOption.style.cssText = ` flex: 1; padding: 9px; text-align: center; cursor: pointer; transition: all 0.2s ease; background: ${!isRelativeMode ? colors.primary : 'transparent'}; color: ${!isRelativeMode ? 'white' : colors.textPrimary}; font-weight: ${!isRelativeMode ? 'bold' : 'normal'}; `; fixedOption.textContent = 'Fixed Prices'; // Relative prices option const relativeOption = document.createElement('div'); relativeOption.style.cssText = ` flex: 1; padding: 9px; text-align: center; cursor: pointer; transition: all 0.2s ease; background: ${isRelativeMode ? colors.primary : 'transparent'}; color: ${isRelativeMode ? 'white' : colors.textPrimary}; font-weight: ${isRelativeMode ? 'bold' : 'normal'}; `; relativeOption.textContent = 'Relative Prices'; toggleContainer.appendChild(fixedOption); toggleContainer.appendChild(relativeOption); pricingModeSection.appendChild(pricingModeLabel); pricingModeSection.appendChild(toggleContainer); // Function to update toggle styles function updateToggleStyles() { const activeSeller = sellerData.sellers[sellerData.activeSeller]; const isRelative = activeSeller.pricingMode === 'relative'; if (DEBUG_MODE) console.log('RW: Updating toggle for seller', sellerData.activeSeller, 'mode:', activeSeller.pricingMode); fixedOption.style.background = !isRelative ? colors.primary : 'transparent'; fixedOption.style.color = !isRelative ? 'white' : colors.textPrimary; fixedOption.style.fontWeight = !isRelative ? 'bold' : 'normal'; relativeOption.style.background = isRelative ? colors.primary : 'transparent'; relativeOption.style.color = isRelative ? 'white' : colors.textPrimary; relativeOption.style.fontWeight = isRelative ? 'bold' : 'normal'; } // Click handlers for toggle fixedOption.addEventListener('click', function() { const currentSeller = sellerData.sellers[sellerData.activeSeller]; if (currentSeller.pricingMode !== 'fixed') { // Clear prices when switching modes to prevent confusion cacheTypes.forEach(function(cache) { currentSeller.prices[cache.key] = 0; }); currentSeller.pricingMode = 'fixed'; updateToggleStyles(); updateInputDisplayMode(); saveSettings(); } }); relativeOption.addEventListener('click', function() { const currentSeller = sellerData.sellers[sellerData.activeSeller]; if (currentSeller.pricingMode !== 'relative') { // Clear prices when switching modes to prevent confusion cacheTypes.forEach(function(cache) { currentSeller.prices[cache.key] = 0; }); currentSeller.pricingMode = 'relative'; updateToggleStyles(); updateInputDisplayMode(); saveSettings(); } }); // Create price inputs const pricesSection = document.createElement('div'); pricesSection.style.marginBottom = '20px'; const cacheTypes = [ { key: 'armorCache', label: 'Armor Cache' }, { key: 'heavyArmsCache', label: 'Heavy Arms' }, { key: 'mediumArmsCache', label: 'Medium Arms' }, { key: 'meleeCache', label: 'Melee Cache' }, { key: 'smallArmsCache', label: 'Small Arms' } ]; const priceInputs = {}; // Function to update input display mode function updateInputDisplayMode() { const activeSeller = sellerData.sellers[sellerData.activeSeller]; const isRelative = activeSeller.pricingMode === 'relative'; if (DEBUG_MODE) console.log('RW: Updating input mode for seller', sellerData.activeSeller, 'mode:', activeSeller.pricingMode); cacheTypes.forEach(function(cache, index) { const input = priceInputs[cache.key]; if (input) { if (isRelative) { // Convert to percentage display (add % suffix, show as percentage) const value = activeSeller.prices[cache.key]; input.value = value > 0 ? value + '%' : '%'; input.style.textAlign = 'center'; // Add placeholder only to first field (Armor Cache) and CSS class if (index === 0) { input.placeholder = 'e.g. 95'; input.classList.add('rw-relative-input'); } else { input.placeholder = ''; input.classList.add('rw-relative-input'); } } else { // Convert to fixed price display (remove % suffix, format as currency) const value = activeSeller.prices[cache.key]; input.value = value > 0 ? formatNumberWithCommas(value) : ''; input.style.textAlign = 'center'; input.placeholder = ''; input.classList.remove('rw-relative-input'); } } }); } cacheTypes.forEach(function(cache) { const row = document.createElement('div'); row.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;'; const label = document.createElement('label'); label.textContent = cache.label + ':'; label.style.cssText = 'color: ' + colors.textPrimary + '; width: 35%;'; const input = document.createElement('input'); input.type = 'text'; input.style.cssText = 'background: ' + colors.inputBg + '; border: 1px solid ' + colors.inputBorder + '; color: ' + colors.textPrimary + '; padding: 8px; border-radius: 4px; width: 60%; text-align: center;'; // Set initial value based on pricing mode const currentSeller = sellerData.sellers[sellerData.activeSeller]; const isRelative = currentSeller.pricingMode === 'relative'; if (isRelative) { const value = currentSeller.prices[cache.key]; input.value = value > 0 ? value + '%' : '%'; input.style.textAlign = 'center'; // Add placeholder only to first field (Armor Cache) and CSS class input.placeholder = cache.key === 'armorCache' ? 'e.g. 95' : ''; input.classList.add('rw-relative-input'); } else { input.value = formatNumberWithCommas(currentSeller.prices[cache.key]); input.style.textAlign = 'center'; input.placeholder = ''; } // Dynamic input formatting based on pricing mode input.addEventListener('input', function() { const currentSeller = sellerData.sellers[sellerData.activeSeller]; const isRelative = currentSeller.pricingMode === 'relative'; if (isRelative) { // Handle percentage input with permanent % symbol let value = input.value.replace(/[^0-9.]/g, ''); // Extract only numbers and decimals if (value) { const numValue = parseFloat(value); if (!isNaN(numValue)) { // Cap at 999% to prevent 4-digit percentages const cappedValue = Math.min(numValue, 999); input.value = cappedValue + '%'; } } else { // Always maintain % symbol even when empty input.value = '%'; } } else { // Handle fixed price input with comma formatting const cursorPosition = input.selectionStart; const oldValue = input.value; const numericValue = input.value.replace(/[^0-9]/g, ''); const formattedValue = numericValue ? formatNumberWithCommas(parseInt(numericValue)) : ''; if (formattedValue !== oldValue) { input.value = formattedValue; // Adjust cursor position after formatting const newCursorPosition = cursorPosition + (formattedValue.length - oldValue.length); input.setSelectionRange(newCursorPosition, newCursorPosition); } } }); priceInputs[cache.key] = input; row.appendChild(label); row.appendChild(input); pricesSection.appendChild(row); }); // Create button section const buttonSection = document.createElement('div'); buttonSection.style.cssText = 'display: flex; gap: 10px; margin-top: 20px; padding-top: 15px; border-top: 1px solid ' + colors.configBorder + ';'; // Create confirmation section for Clear All Sellers (initially hidden) const clearConfirmationSection = document.createElement('div'); clearConfirmationSection.id = 'clear-confirmation'; clearConfirmationSection.style.cssText = 'display: none; margin-top: 15px; padding: 15px; background: ' + colors.panelBg + '; border: 1px solid ' + colors.danger + '; border-radius: 4px;'; const clearConfirmText = document.createElement('div'); clearConfirmText.textContent = 'Reset all traders to default? This will clear all custom trader names and prices.'; clearConfirmText.style.cssText = 'color: ' + colors.textPrimary + '; margin-bottom: 10px; font-size: 14px;'; const clearConfirmButtons = document.createElement('div'); clearConfirmButtons.style.cssText = 'display: flex; gap: 10px;'; const clearConfirmYes = document.createElement('button'); clearConfirmYes.textContent = 'Yes, Reset All'; clearConfirmYes.style.cssText = 'background: ' + colors.danger + '; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; flex: 1;'; const clearConfirmNo = document.createElement('button'); clearConfirmNo.textContent = 'Cancel'; clearConfirmNo.style.cssText = 'background: ' + colors.textSecondary + '; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; flex: 1;'; clearConfirmButtons.appendChild(clearConfirmYes); clearConfirmButtons.appendChild(clearConfirmNo); clearConfirmationSection.appendChild(clearConfirmText); clearConfirmationSection.appendChild(clearConfirmButtons); // Create confirmation section for Delete Seller (initially hidden) const deleteConfirmationSection = document.createElement('div'); deleteConfirmationSection.id = 'delete-confirmation'; deleteConfirmationSection.style.cssText = 'display: none; margin-top: 15px; padding: 15px; background: ' + colors.panelBg + '; border: 1px solid ' + colors.danger + '; border-radius: 4px;'; const deleteConfirmText = document.createElement('div'); deleteConfirmText.textContent = 'Delete this trader configuration? This will reset name and set prices to zero.'; deleteConfirmText.style.cssText = 'color: ' + colors.textPrimary + '; margin-bottom: 10px; font-size: 14px;'; const deleteConfirmButtons = document.createElement('div'); deleteConfirmButtons.style.cssText = 'display: flex; gap: 10px;'; const deleteConfirmYes = document.createElement('button'); deleteConfirmYes.textContent = mobile ? 'Delete Trader' : 'Yes, Delete Trader'; deleteConfirmYes.style.cssText = 'background: ' + colors.danger + '; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; flex: 1; font-size: ' + (mobile ? '11px' : '12px') + ';'; const deleteConfirmNo = document.createElement('button'); deleteConfirmNo.textContent = 'Cancel'; deleteConfirmNo.style.cssText = 'background: ' + colors.textSecondary + '; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; flex: 1;'; deleteConfirmButtons.appendChild(deleteConfirmYes); deleteConfirmButtons.appendChild(deleteConfirmNo); deleteConfirmationSection.appendChild(deleteConfirmText); deleteConfirmationSection.appendChild(deleteConfirmButtons); const copyFromApiBtn = document.createElement('button'); copyFromApiBtn.textContent = mobile ? 'Copy API' : 'Copy from API'; copyFromApiBtn.style.cssText = 'background: #454545; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; flex: 1; font-size: ' + (mobile ? '11px' : '12px') + ';'; const deleteBtn = document.createElement('button'); deleteBtn.textContent = mobile ? 'Delete' : 'Delete Trader'; deleteBtn.style.cssText = 'background: ' + colors.danger + '; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; flex: 1; font-size: ' + (mobile ? '11px' : '12px') + ';'; const saveBtn = document.createElement('button'); saveBtn.textContent = 'Save'; saveBtn.style.cssText = 'background: ' + colors.success + '; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; flex: 1; font-size: ' + (mobile ? '11px' : '12px') + ';'; const clearSellersBtn = document.createElement('button'); clearSellersBtn.textContent = mobile ? 'Clear All' : 'Clear All Traders'; clearSellersBtn.style.cssText = 'background: ' + colors.danger + '; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; flex: 1; font-size: ' + (mobile ? '11px' : '12px') + ';'; buttonSection.appendChild(copyFromApiBtn); buttonSection.appendChild(clearSellersBtn); buttonSection.appendChild(deleteBtn); buttonSection.appendChild(saveBtn); // Event handlers let editMode = false; // Track if we're editing or adding new // Function to show name input function showNameInput(isEdit, currentName) { editMode = isEdit; nameInput.value = currentName || ''; nameInputContainer.style.display = 'block'; nameLabel.textContent = isEdit ? 'Edit Trader Name e.g. \'John Doe [123456]\'' : 'New Trader Name e.g. \'John Doe [123456]\''; nameInput.focus(); } // Function to hide name input function hideNameInput() { nameInputContainer.style.display = 'none'; nameInput.value = ''; } // Edit seller button editSellerBtn.addEventListener('click', function() { const currentSeller = sellerData.sellers[sellerData.activeSeller]; showNameInput(true, currentSeller.name); }); // Save name button saveNameBtn.addEventListener('click', function() { const name = nameInput.value.trim(); if (!name) { alert('Please enter a trader name'); return; } const userInfo = extractUserInfo(name); if (editMode) { // Edit existing seller sellerData.sellers[sellerData.activeSeller].name = name; // Update dropdown option const option = sellerSelect.options[sellerData.activeSeller]; option.textContent = userInfo.name + (userInfo.hasId ? ' [' + userInfo.id + ']' : ''); } else { // Add new seller sellerData.sellers.push({ name: name, pricingMode: "fixed", prices: { armorCache: 0, heavyArmsCache: 0, mediumArmsCache: 0, meleeCache: 0, smallArmsCache: 0 } }); // Add to dropdown const option = document.createElement('option'); option.value = sellerData.sellers.length - 1; option.textContent = userInfo.name + (userInfo.hasId ? ' [' + userInfo.id + ']' : ''); sellerSelect.appendChild(option); // Select new seller sellerSelect.value = sellerData.sellers.length - 1; sellerData.activeSeller = sellerData.sellers.length - 1; // Update price inputs cacheTypes.forEach(function(cache) { priceInputs[cache.key].value = '0'; }); } // Save seller changes saveSettings(); hideNameInput(); }); // Cancel name button cancelNameBtn.addEventListener('click', function() { hideNameInput(); }); // Allow Enter key to save nameInput.addEventListener('keypress', function(e) { if (e.key === 'Enter') { saveNameBtn.click(); } }); sellerSelect.onchange = function() { sellerData.activeSeller = parseInt(sellerSelect.value); // Update toggle styles and input display mode updateToggleStyles(); updateInputDisplayMode(); // Update last modified date display const newSeller = sellerData.sellers[sellerData.activeSeller]; lastModifiedDiv.textContent = formatLastModified(newSeller.lastModified); // Hide name input if open hideNameInput(); // Save the active seller change saveSettings(); // Just update the seller display - values will refresh when panel closes updateSellerDisplay(); }; // Function to update just the seller name in the combined total function updateSellerDisplay() { const combinedTotalContainers = document.querySelectorAll('#rw-panels-container'); combinedTotalContainers.forEach(function(container) { // Find seller spans and update them const sellerSpans = container.querySelectorAll('span[style*="margin-left: 16px"]'); sellerSpans.forEach(function(span) { if (span.textContent.includes('Trader:')) { const currentSeller = sellerData.sellers[sellerData.activeSeller]; const userInfo = extractUserInfo(currentSeller.name); // Clear existing content span.innerHTML = ''; span.textContent = 'Trader: ' + userInfo.name; // Re-add profile link if exists if (userInfo.hasId) { const colors = getThemeColors(); const profileLink = createProfileLink(userInfo.id, colors); span.appendChild(profileLink); } } }); }); } saveBtn.onclick = function() { console.log('RW: Save button clicked'); // Save current prices and track if any changed let pricesChanged = false; const currentSeller = sellerData.sellers[sellerData.activeSeller]; const isRelative = currentSeller.pricingMode === 'relative'; cacheTypes.forEach(function(cache) { const oldPrice = currentSeller.prices[cache.key]; let newPrice = 0; if (isRelative) { // Parse percentage value (remove % and parse as decimal) const percentValue = priceInputs[cache.key].value.replace('%', '').trim(); newPrice = percentValue ? parseFloat(percentValue) : 0; } else { // Parse fixed price value (remove commas) newPrice = parseCommaNumber(priceInputs[cache.key].value); } currentSeller.prices[cache.key] = newPrice; if (oldPrice !== newPrice) { if (DEBUG_MODE) console.log('RW: Price changed for', cache.key, 'from', oldPrice, 'to', newPrice, 'mode:', currentSeller.pricingMode); pricesChanged = true; } }); // Update last modified date if any prices changed if (pricesChanged) { sellerData.sellers[sellerData.activeSeller].lastModified = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format } // Save seller data to localStorage saveSettings(); if (DEBUG_MODE) { console.log('RW: Saved new prices for', sellerData.sellers[sellerData.activeSeller].name); console.log('RW: New prices:', sellerData.sellers[sellerData.activeSeller].prices); } // Close panel and use full refresh (reliable) panel.style.maxHeight = '0'; panel.style.padding = '0 15px'; setTimeout(function() { panel.remove(); console.log('RW: Price panel closed, using full refresh for reliability'); refreshRewardDisplay(); }, 300); }; copyFromApiBtn.onclick = async function() { copyFromApiBtn.textContent = 'Loading...'; copyFromApiBtn.disabled = true; const result = await fetchApiPrices(); if (result.success) { // Update the price inputs with API values Object.keys(result.prices).forEach(function(cacheType) { const price = result.prices[cacheType]; if (priceInputs[cacheType]) { priceInputs[cacheType].value = formatNumberWithCommas(price); } }); copyFromApiBtn.textContent = 'Copied!'; copyFromApiBtn.style.background = colors.success; setTimeout(function() { copyFromApiBtn.textContent = mobile ? 'Copy API' : 'Copy from API'; copyFromApiBtn.style.background = '#454545'; copyFromApiBtn.disabled = false; }, 2000); } else { alert('Failed to fetch API prices: ' + result.error); copyFromApiBtn.textContent = mobile ? 'Copy API' : 'Copy from API'; copyFromApiBtn.disabled = false; } }; clearSellersBtn.onclick = function() { // Hide delete confirmation if shown deleteConfirmationSection.style.display = 'none'; deleteBtn.disabled = false; deleteBtn.style.opacity = '1'; // Show clear confirmation section clearConfirmationSection.style.display = 'block'; clearSellersBtn.disabled = true; clearSellersBtn.style.opacity = '0.5'; }; deleteBtn.onclick = function() { // Hide clear confirmation if shown clearConfirmationSection.style.display = 'none'; clearSellersBtn.disabled = false; clearSellersBtn.style.opacity = '1'; // Show delete confirmation section deleteConfirmationSection.style.display = 'block'; deleteBtn.disabled = true; deleteBtn.style.opacity = '0.5'; }; // Clear All Sellers confirmation handlers clearConfirmYes.onclick = function() { // Reset to default sellers sellerData = { activeSeller: 0, sellers: [ { name: "Trader 1", prices: { armorCache: 0, heavyArmsCache: 0, mediumArmsCache: 0, meleeCache: 0, smallArmsCache: 0 } }, { name: "Trader 2", prices: { armorCache: 0, heavyArmsCache: 0, mediumArmsCache: 0, meleeCache: 0, smallArmsCache: 0 } }, { name: "Trader 3", prices: { armorCache: 0, heavyArmsCache: 0, mediumArmsCache: 0, meleeCache: 0, smallArmsCache: 0 } } ] }; // Save to localStorage saveSettings(); // Refresh the panel by closing and reopening it panel.style.maxHeight = '0'; panel.style.padding = '0 15px'; setTimeout(function() { panel.remove(); // Reopen the panel to show reset sellers setTimeout(function() { showPricePanel(); }, 100); }, 300); }; clearConfirmNo.onclick = function() { // Hide confirmation section clearConfirmationSection.style.display = 'none'; clearSellersBtn.disabled = false; clearSellersBtn.style.opacity = '1'; }; // Delete Seller confirmation handlers deleteConfirmYes.onclick = function() { const currentIndex = sellerData.activeSeller; // Determine the default name for this slot const defaultNames = ["Trader 1", "Trader 2", "Trader 3", "Trader 4", "Trader 5", "Trader 6", "Trader 7", "Trader 8", "Trader 9", "Trader 10"]; const defaultName = defaultNames[currentIndex] || "Seller " + (currentIndex + 1); // Reset seller to default state sellerData.sellers[currentIndex] = { name: defaultName, prices: { armorCache: 0, heavyArmsCache: 0, mediumArmsCache: 0, meleeCache: 0, smallArmsCache: 0 } }; // Update the dropdown option const option = sellerSelect.options[currentIndex]; option.textContent = defaultName; // Update price inputs to show zeros cacheTypes.forEach(function(cache) { priceInputs[cache.key].value = '0'; }); // Save the changes saveSettings(); // Hide confirmation deleteConfirmationSection.style.display = 'none'; deleteBtn.disabled = false; deleteBtn.style.opacity = '1'; }; deleteConfirmNo.onclick = function() { // Hide confirmation section deleteConfirmationSection.style.display = 'none'; deleteBtn.disabled = false; deleteBtn.style.opacity = '1'; }; // Assemble panel panel.appendChild(header); panel.appendChild(sellerSection); panel.appendChild(pricingModeSection); panel.appendChild(pricesSection); panel.appendChild(buttonSection); panel.appendChild(clearConfirmationSection); panel.appendChild(deleteConfirmationSection); // Insert after the title header titleHeader.insertAdjacentElement('afterend', panel); } // ======================================== // CONFIGURATION PANELS // ======================================== function showSettingsPanel() { const colors = getThemeColors(); const mobile = isMobile(); // Close any existing panel const existingPanel = document.getElementById('rw-settings-panel'); if (existingPanel) { existingPanel.remove(); return; // Toggle off } // Close price panel if open const pricePanel = document.getElementById('rw-price-panel'); if (pricePanel) pricePanel.remove(); // Find the title header to attach panel after const titleHeader = document.querySelector('.title-black.m-top10.top-round') || document.querySelector('.title-black') || document.querySelector('[class*="title"]'); if (!titleHeader) return; // Create slide-down panel const panel = document.createElement('div'); panel.id = 'rw-settings-panel'; panel.style.cssText = 'background: ' + colors.configBg + '; border: none; border-radius: 0; padding: 15px; margin: 0; color: ' + colors.textPrimary + '; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; box-shadow: 0 4px 8px rgba(0,0,0,0.15); position: relative; overflow: hidden; max-height: 0; transition: max-height 0.3s ease, padding 0.3s ease;'; // Animate in - increase height for PDA mode to accommodate extra info box setTimeout(function() { panel.style.maxHeight = PDA_MODE ? '550px' : '500px'; }, 10); // Create header const header = document.createElement('div'); header.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid ' + colors.configBorder + ';'; const title = document.createElement('h3'); title.textContent = 'Settings'; title.style.cssText = 'margin: 0; color: ' + colors.textPrimary + '; font-size: 18px;'; const closeBtn = document.createElement('button'); // Use HTML entity for mobile compatibility if (isMobile()) { closeBtn.innerHTML = '✖'; } else { closeBtn.textContent = '✕'; } closeBtn.style.cssText = 'background: none; border: none; font-size: 20px; color: ' + colors.textSecondary + '; cursor: pointer; padding: 5px; border-radius: 3px;'; closeBtn.onclick = function() { panel.style.maxHeight = '0'; panel.style.padding = '0 15px'; setTimeout(function() { panel.remove(); // Refresh the display to update info box visibility based on new settings refreshRewardDisplay(); }, 300); }; header.appendChild(title); header.appendChild(closeBtn); // API Configuration Section const apiSection = document.createElement('div'); apiSection.style.marginBottom = '25px'; const apiTitle = document.createElement('h4'); apiTitle.textContent = 'API Configuration'; apiTitle.style.cssText = 'margin: 0 0 15px 0; color: ' + colors.textPrimary + '; font-size: 16px;'; apiSection.appendChild(apiTitle); const keyContainer = document.createElement('div'); keyContainer.style.cssText = mobile ? 'display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px;' : 'display: flex; gap: 8px; align-items: center; margin-bottom: 10px;'; const keyLabel = document.createElement('label'); keyLabel.textContent = 'Public API Key:'; keyLabel.style.cssText = 'color: ' + colors.textPrimary + '; ' + (mobile ? 'margin-bottom: 5px;' : 'width: 100px;'); const inputRow = document.createElement('div'); inputRow.style.cssText = 'display: flex; gap: 8px; align-items: center;' + (mobile ? ' flex-wrap: wrap; margin-bottom: 0;' : ''); const keyInput = document.createElement('input'); keyInput.type = 'password'; keyInput.value = API_KEY !== 'YOUR_API_KEY_HERE' ? API_KEY : ''; keyInput.placeholder = 'Enter your Torn API key'; // Disable browser autocomplete/autofill to prevent login suggestions // Note: These attributes only prevent browser behaviour such as autofill keyInput.autocomplete = 'new-password'; // More effective than 'off' on mobile browsers keyInput.spellcheck = false; keyInput.setAttribute('data-form-type', 'other'); // Additional hint this isn't a login form keyInput.style.cssText = 'background: ' + colors.inputBg + '; border: 1px solid ' + colors.inputBorder + '; color: ' + colors.textPrimary + '; padding: 8px; border-radius: 4px; ' + (mobile ? 'width: 100%; margin-bottom: 0;' : 'flex: 1;'); const buttonRow = document.createElement('div'); buttonRow.style.cssText = mobile ? 'display: flex; gap: 6px; width: 100%; flex-wrap: wrap;' : 'display: flex; gap: 8px;'; const editBtn = document.createElement('button'); editBtn.textContent = 'Edit'; const editBtnColor = currentTheme === 'light' ? '#999999' : colors.textSecondary; editBtn.style.cssText = 'background: ' + editBtnColor + ' !important; color: white !important; border: none !important; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; ' + (mobile ? 'flex: 1; min-width: 0;' : ''); const testBtn = document.createElement('button'); testBtn.textContent = 'Validate'; testBtn.style.cssText = 'background: ' + colors.primary + '; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; ' + (mobile ? 'flex: 1; min-width: 0;' : ''); const resetApiBtn = document.createElement('button'); resetApiBtn.textContent = 'Reset'; resetApiBtn.style.cssText = 'background: ' + colors.danger + '; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; ' + (mobile ? 'flex: 1; min-width: 0;' : ''); resetApiBtn.title = 'Clear API key and cache'; // Create status div first const statusDiv = document.createElement('div'); statusDiv.style.cssText = 'margin-left: ' + (mobile ? '0px' : '110px') + '; font-size: 12px; color: ' + colors.textMuted + '; ' + (mobile ? 'text-align: left; margin-top: 4px; margin-bottom: 12px;' : ''); const statusPrefix = mobile ? 'Status: ' : 'Status: '; // Use HTML entities for mobile compatibility const checkmark = mobile ? '✅' : '✅'; const cross = mobile ? '❌' : '❌'; // Improved status logic for better PDA handling let isConfigured = false; let statusMessage = ''; if (API_KEY !== 'YOUR_API_KEY_HERE') { // Manual API key configured isConfigured = true; statusMessage = 'Valid - Welcome ' + (API_USERNAME || 'User'); } else if (PDA_MODE && PDA_VALIDATED) { // PDA mode with validation - don't require API_USERNAME to show as valid isConfigured = true; statusMessage = 'Valid - Welcome ' + (API_USERNAME || 'PDA User') + ' (PDA)'; } else if (PDA_MODE && API_USERNAME) { // PDA mode with username but no validation flag - could be a recovery case isConfigured = true; statusMessage = 'Valid - Welcome ' + API_USERNAME + ' (PDA - reconnected)'; console.log('RW: PDA reconnection detected - username exists but validation flag missing'); // Restore validation flag PDA_VALIDATED = true; saveSettings(); } const validationSuccessColor = currentTheme === 'light' ? '#69a829' : colors.success; statusDiv.innerHTML = isConfigured ? statusPrefix + '<span style="color: ' + validationSuccessColor + ';">' + checkmark + ' ' + statusMessage + '</span>' : statusPrefix + '<span style="color: ' + colors.danger + ';">' + cross + ' Not configured</span>'; if (mobile) { keyContainer.appendChild(keyLabel); inputRow.appendChild(keyInput); keyContainer.appendChild(inputRow); keyContainer.appendChild(statusDiv); // Status div positioned between input and buttons on mobile buttonRow.appendChild(editBtn); buttonRow.appendChild(testBtn); buttonRow.appendChild(resetApiBtn); keyContainer.appendChild(buttonRow); } else { inputRow.appendChild(keyLabel); inputRow.appendChild(keyInput); inputRow.appendChild(editBtn); inputRow.appendChild(testBtn); inputRow.appendChild(resetApiBtn); keyContainer.appendChild(inputRow); } // Create last retrieved info (moved up from API values section) const lastRetrievedDiv = document.createElement('div'); lastRetrievedDiv.style.cssText = mobile ? 'margin-left: 0px; margin-top: 8px; font-size: 11px; color: ' + colors.textMuted + ';' : 'margin-left: 110px; margin-top: 4px; font-size: 11px; color: ' + colors.textMuted + ';'; function getLastRetrievedText() { if (!apiPriceCache.lastFetched) return 'Last retrieved: Never'; const lastFetched = new Date(apiPriceCache.lastFetched); const now = new Date(); const diffMs = now - lastFetched; const diffMins = Math.floor(diffMs / (1000 * 60)); const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); const timeString = lastFetched.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }); if (diffDays === 0) { return 'Last retrieved: Today at ' + timeString; } else if (diffDays === 1) { return 'Last retrieved: Yesterday at ' + timeString; } else { const day = lastFetched.getDate(); const month = lastFetched.toLocaleString('en-US', { month: 'short' }); return 'Last retrieved: ' + day + ' ' + month + ' at ' + timeString; } } function updateLastRetrievedDisplay() { const uniqueId = 'refresh-api-link-' + Date.now(); // Unique ID to avoid conflicts lastRetrievedDiv.innerHTML = getLastRetrievedText() + (apiPriceCache.lastFetched ? ' <span id="' + uniqueId + '" style="color: ' + colors.primary + '; cursor: pointer; text-decoration: underline;">Refresh</span>' : ''); // Add event listener for refresh link if it exists const refreshLink = document.getElementById(uniqueId); if (refreshLink) { refreshLink.addEventListener('click', function() { console.log('RW: Manual API refresh requested'); refreshLink.textContent = 'Loading...'; refreshLink.style.cursor = 'wait'; updateApiPriceCache(true).then(function(success) { // Force refresh for manual clicks if (success) { console.log('RW: Manual API refresh successful'); updateLastRetrievedDisplay(); refreshRewardDisplay(); } else { console.log('RW: Manual API refresh failed'); refreshLink.textContent = 'Failed'; setTimeout(function() { updateLastRetrievedDisplay(); }, 2000); } }); }); } else { console.log('RW: Refresh link not found with ID:', uniqueId); } } apiSection.appendChild(apiTitle); apiSection.appendChild(keyContainer); if (!mobile) { apiSection.appendChild(statusDiv); // Status div positioned after keyContainer on desktop apiSection.appendChild(lastRetrievedDiv); // Last retrieved positioned after status on desktop } else { // On mobile, add last retrieved inside keyContainer after all other elements keyContainer.appendChild(lastRetrievedDiv); } // Update the display after the element is in the DOM with a small delay setTimeout(function() { updateLastRetrievedDisplay(); }, 10); // Add PDA-specific instruction if detected - MOVED TO AFTER OTHER ELEMENTS if (PDA_MODE) { const pdaInfo = document.createElement('div'); pdaInfo.style.cssText = 'margin-top: 15px; margin-bottom: 12px; padding: 8px 12px; background: ' + colors.statBoxBg + '; border: 1px solid ' + colors.primary + '; border-radius: 4px; font-size: 11px; color: ' + colors.textPrimary + ';'; pdaInfo.innerHTML = '<strong>Torn PDA detected:</strong> If you\'ve already connected your API key to PDA, just click validate.'; apiSection.appendChild(pdaInfo); } // Other Settings Section const otherSection = document.createElement('div'); otherSection.style.marginBottom = '20px'; const otherTitle = document.createElement('h4'); otherTitle.textContent = 'Other Settings'; otherTitle.style.cssText = 'margin: 0 0 15px 0; color: ' + colors.textPrimary + '; font-size: 16px;'; // Custom Prices section const customPricesSection = document.createElement('div'); customPricesSection.style.cssText = 'margin-bottom: 15px;'; const showCustomPricesCheck = document.createElement('div'); showCustomPricesCheck.style.cssText = 'display: flex; align-items: center; margin-bottom: 2px;'; const customPricesCheckbox = document.createElement('input'); customPricesCheckbox.type = 'checkbox'; customPricesCheckbox.checked = SETTINGS.showCustomPrices; const customPricesCheckboxStyle = 'margin-right: 8px;' + (currentTheme === 'dark' ? ' accent-color: #74c0fc;' : ''); customPricesCheckbox.style.cssText = customPricesCheckboxStyle; const customPricesLabel = document.createElement('label'); customPricesLabel.textContent = 'Show custom prices'; customPricesLabel.style.color = colors.textPrimary; customPricesLabel.style.cursor = 'pointer'; // Make label clickable to toggle checkbox customPricesLabel.addEventListener('click', function() { customPricesCheckbox.checked = !customPricesCheckbox.checked; // Trigger the change event to ensure all logic runs customPricesCheckbox.dispatchEvent(new Event('change')); }); // Handle checkbox change customPricesCheckbox.addEventListener('change', function() { SETTINGS.showCustomPrices = this.checked; saveSettings(); // Update the "Show API alongside" checkbox state immediately const customPricesEnabled = this.checked; showCheckbox.disabled = !customPricesEnabled; showLabel.style.color = customPricesEnabled ? colors.textPrimary : colors.textMuted; showLabel.style.cursor = customPricesEnabled ? 'pointer' : 'not-allowed'; // If disabling custom prices, also disable the API alongside option if (!customPricesEnabled) { SETTINGS.showApiValues = false; showCheckbox.checked = false; saveSettings(); } // Refresh display to apply custom price changes immediately refreshRewardDisplay(); }); showCustomPricesCheck.appendChild(customPricesCheckbox); showCustomPricesCheck.appendChild(customPricesLabel); customPricesSection.appendChild(showCustomPricesCheck); // API Values section with combined checkbox and last retrieved const apiValuesSection = document.createElement('div'); apiValuesSection.style.cssText = 'margin-bottom: 15px;'; const showApiCheck = document.createElement('div'); showApiCheck.style.cssText = 'display: flex; align-items: center; margin-bottom: 2px;'; const showCheckbox = document.createElement('input'); showCheckbox.type = 'checkbox'; showCheckbox.checked = SETTINGS.showApiValues; // Only disable if custom prices are not enabled const customPricesDisabled = !SETTINGS.showCustomPrices; showCheckbox.disabled = customPricesDisabled; const checkboxStyle = 'margin-right: 8px;' + (currentTheme === 'dark' ? ' accent-color: #74c0fc;' : ''); showCheckbox.style.cssText = checkboxStyle; const showLabel = document.createElement('label'); showLabel.textContent = 'Show API market values alongside custom prices'; // Grey out if disabled for any reason showLabel.style.color = showCheckbox.disabled ? colors.textMuted : colors.textPrimary; showLabel.style.cursor = showCheckbox.disabled ? 'not-allowed' : 'pointer'; // Make label clickable to toggle checkbox showLabel.addEventListener('click', function() { if (!showCheckbox.disabled) { showCheckbox.checked = !showCheckbox.checked; } }); // Note: API key input no longer affects the "Show API alongside" checkbox // That checkbox is now only controlled by the "Show custom prices" setting // Update display when checkbox changes showCheckbox.addEventListener('change', function() { SETTINGS.showApiValues = this.checked; saveSettings(); }); showApiCheck.appendChild(showCheckbox); showApiCheck.appendChild(showLabel); // Assemble API values section apiValuesSection.appendChild(showApiCheck); otherSection.appendChild(otherTitle); otherSection.appendChild(customPricesSection); otherSection.appendChild(apiValuesSection); // Indirect rewards section const showIndirectCheck = document.createElement('div'); showIndirectCheck.style.cssText = 'display: flex; align-items: center; margin-bottom: 15px;'; const indirectCheckbox = document.createElement('input'); indirectCheckbox.type = 'checkbox'; indirectCheckbox.checked = SETTINGS.showIndirectRewards; const indirectCheckboxStyle = 'margin-right: 8px;' + (currentTheme === 'dark' ? ' accent-color: #74c0fc;' : ''); indirectCheckbox.style.cssText = indirectCheckboxStyle; const indirectLabel = document.createElement('label'); indirectLabel.textContent = mobile ? 'Show other rewards incl points and respect' : 'Show other rewards including points and respect'; indirectLabel.style.color = colors.textPrimary; indirectLabel.style.cursor = 'pointer'; // Make label clickable to toggle checkbox indirectLabel.addEventListener('click', function() { indirectCheckbox.checked = !indirectCheckbox.checked; }); showIndirectCheck.appendChild(indirectCheckbox); showIndirectCheck.appendChild(indirectLabel); // Respect value input section const respectValueSection = document.createElement('div'); respectValueSection.style.cssText = mobile ? 'display: flex; gap: 8px; align-items: center; margin-bottom: 15px; padding-left: 26px;' : 'display: flex; gap: 8px; align-items: center; margin-bottom: 15px;'; const respectLabel = document.createElement('label'); respectLabel.textContent = 'Value per respect:'; respectLabel.style.cssText = 'color: ' + colors.textPrimary + '; ' + (mobile ? 'width: 100px; white-space: nowrap;' : 'width: 100px;'); const respectInputRow = document.createElement('div'); respectInputRow.style.cssText = 'display: flex; gap: 8px; align-items: center;' + (mobile ? ' flex-wrap: wrap; margin-bottom: 0;' : ''); const respectInput = document.createElement('input'); respectInput.type = 'text'; respectInput.value = formatNumberWithCommas(SETTINGS.respectValue); respectInput.placeholder = '20,000'; respectInput.style.cssText = 'background: ' + colors.inputBg + '; border: 1px solid ' + colors.inputBorder + '; color: ' + colors.textPrimary + '; padding: 8px; border-radius: 4px; text-align: right; ' + (mobile ? 'width: 100px;' : 'width: 120px;'); // Add real-time comma formatting for respect input respectInput.addEventListener('input', function() { const cursorPosition = respectInput.selectionStart; const oldValue = respectInput.value; const numericValue = respectInput.value.replace(/[^0-9]/g, ''); const formattedValue = numericValue ? formatNumberWithCommas(parseInt(numericValue)) : ''; if (formattedValue !== oldValue) { respectInput.value = formattedValue; // Adjust cursor position after formatting const newCursorPosition = cursorPosition + (formattedValue.length - oldValue.length); respectInput.setSelectionRange(newCursorPosition, newCursorPosition); } }); respectInputRow.appendChild(respectInput); if (mobile) { respectValueSection.appendChild(respectLabel); respectValueSection.appendChild(respectInputRow); } else { respectValueSection.appendChild(respectLabel); respectValueSection.appendChild(respectInputRow); } otherSection.appendChild(showIndirectCheck); otherSection.appendChild(respectValueSection); // Save button const saveBtn = document.createElement('button'); saveBtn.textContent = 'Save'; const saveBtnColor = currentTheme === 'light' ? '#69a829' : colors.success; saveBtn.style.cssText = 'background: ' + saveBtnColor + ' !important; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; width: 100%; margin-top: ' + (mobile ? '8px' : '15px') + '; border-top: 1px solid ' + colors.configBorder + '; padding-top: ' + (mobile ? '8px' : '15px') + ';'; // Event handlers editBtn.onclick = function() { keyInput.type = keyInput.type === 'password' ? 'text' : 'password'; editBtn.textContent = keyInput.type === 'password' ? 'Edit' : 'Hide'; }; testBtn.onclick = async function() { // Allow validation with empty field if in PDA mode if (!keyInput.value.trim() && !PDA_MODE) { alert('Please enter an API key first.'); return; } testBtn.textContent = 'Testing...'; testBtn.disabled = true; try { const result = await testApiKey(keyInput.value.trim()); if (result.success) { const checkmark = mobile ? '✅' : '✅'; const pdaIndicator = result.isPDA ? ' (PDA)' : ''; const dynamicValidationColor = currentTheme === 'light' ? '#69a829' : colors.success; statusDiv.innerHTML = 'Status: <span style="color: ' + dynamicValidationColor + ';">' + checkmark + ' Valid - Welcome ' + result.name + pdaIndicator + '</span>'; testBtn.textContent = 'Valid!'; testBtn.style.background = colors.success; // Store the validated username API_USERNAME = result.name; // If this was a PDA validation, set PDA validated flag if (result.isPDA) { console.log('RW: PDA State Transition - API validation successful for:', result.name); PDA_VALIDATED = true; console.log('RW: PDA State - Mode:', PDA_MODE, 'Validated:', PDA_VALIDATED, 'Username:', API_USERNAME); } else { // Store manual API key for non-PDA validation console.log('RW: Manual API validation successful for:', result.name); API_KEY = keyInput.value.trim(); PDA_VALIDATED = false; console.log('RW: Manual API State - Key saved, PDA_VALIDATED set to false'); } // FIXED: Save to localStorage immediately - EXPLICIT save if (typeof(Storage) !== "undefined") { localStorage.setItem('rw_api_username', API_USERNAME); if (PDA_VALIDATED) { localStorage.setItem('rw_pda_validated', 'true'); } console.log('RW: EXPLICIT save of username to localStorage:', API_USERNAME); } // Immediately fetch API prices after successful validation console.log('RW: API validation successful - fetching prices immediately...'); updateApiPriceCache().then(function(success) { if (success) { console.log('RW: API prices fetched successfully during validation'); updateLastRetrievedDisplay(); } else { console.log('RW: Failed to fetch API prices during validation'); } }); saveSettings(); } else { const cross = mobile ? '❌' : '❌'; statusDiv.innerHTML = 'Status: <span style="color: ' + colors.danger + ';">' + cross + ' Error: ' + result.error + '</span>'; testBtn.textContent = 'Error'; testBtn.style.background = colors.danger; } } catch (error) { console.error('RW: API validation exception:', error); const cross = mobile ? '❌' : '❌'; statusDiv.innerHTML = 'Status: <span style="color: ' + colors.danger + ';">' + cross + ' Validation failed - please try again</span>'; testBtn.textContent = 'Failed'; testBtn.style.background = colors.danger; } finally { // Always reset button after 3 seconds setTimeout(function() { testBtn.textContent = 'Validate'; testBtn.style.background = colors.primary; testBtn.disabled = false; }, 3000); } }; resetApiBtn.onclick = function() { // Clear API key API_KEY = 'YOUR_API_KEY_HERE'; keyInput.value = ''; // Clear API cache apiPriceCache = { lastFetched: null, data: {} }; // Clear stored username and PDA validation API_USERNAME = ''; PDA_VALIDATED = false; // Disable show API values SETTINGS.showApiValues = false; showCheckbox.checked = false; showCheckbox.disabled = true; showLabel.style.color = colors.textMuted; showLabel.style.cursor = 'not-allowed'; // Update status const cross = mobile ? '❌' : '❌'; statusDiv.innerHTML = 'Status: <span style="color: ' + colors.danger + ';">' + cross + ' Not configured</span>'; // Save settings saveSettings(); // Clear from localStorage if (typeof(Storage) !== "undefined") { localStorage.removeItem('rw_api_key'); localStorage.removeItem('rw_api_username'); localStorage.removeItem('rw_pda_validated'); } resetApiBtn.textContent = 'Cleared!'; resetApiBtn.style.background = colors.success; setTimeout(function() { resetApiBtn.textContent = 'Reset'; resetApiBtn.style.background = colors.danger; }, 2000); // Refresh display to remove API values setTimeout(function() { refreshRewardDisplay(); }, 500); }; saveBtn.onclick = function() { try { // Store previous settings to detect changes const previousApiKey = API_KEY; const previousShowApiValues = SETTINGS.showApiValues; const previousShowIndirectRewards = SETTINGS.showIndirectRewards; const previousRespectValue = SETTINGS.respectValue; // Save API key and username if (keyInput.value.trim() && !PDA_MODE) { // Manual API key for non-PDA users API_KEY = keyInput.value.trim(); console.log('RW: Saved manual API key'); } else if (PDA_MODE && PDA_VALIDATED) { // For PDA users, ensure we maintain the validated state console.log('RW: Maintaining PDA validated state'); } // Store username and validation state if we have it if (API_USERNAME) { console.log('RW: Saving username and validation state:', API_USERNAME, 'PDA_VALIDATED:', PDA_VALIDATED); saveSettings(); // This will save the username and PDA validation state } // Save settings SETTINGS.showApiValues = showCheckbox.checked; SETTINGS.showIndirectRewards = indirectCheckbox.checked; SETTINGS.respectValue = parseCommaNumber(respectInput.value); saveSettings(); // Enhanced debugging logs for API updates console.log('RW: Settings saved - API key changed:', (previousApiKey !== API_KEY)); console.log('RW: API update check - showApiValues:', SETTINGS.showApiValues, 'hasValidKey:', (API_KEY && API_KEY !== 'YOUR_API_KEY_HERE'), 'PDA_VALIDATED:', PDA_VALIDATED); // Handle API-related updates - include PDA validation const apiKeyChanged = previousApiKey !== API_KEY; const hasValidApiKey = ((API_KEY && API_KEY !== 'YOUR_API_KEY_HERE') || (PDA_MODE && PDA_VALIDATED)); const needsApiUpdate = hasValidApiKey; console.log('RW: needsApiUpdate:', needsApiUpdate, 'apiKeyChanged:', apiKeyChanged, 'hasValidApiKey:', hasValidApiKey); if (needsApiUpdate && apiKeyChanged) { console.log('RW: API key available and show API enabled - fetching prices...'); updateApiPriceCache().then(function(success) { if (success) { console.log('RW: API prices fetched successfully, refreshing display'); refreshRewardDisplay(); } else { console.log('RW: API fetch failed, display unchanged'); } }); } else if (!SETTINGS.showApiValues && previousShowApiValues) { console.log('RW: Show API disabled, refreshing display to remove API values'); refreshRewardDisplay(); } // Handle indirect rewards setting changes const indirectRewardsChanged = (SETTINGS.showIndirectRewards !== previousShowIndirectRewards); const respectValueChanged = (SETTINGS.respectValue !== previousRespectValue); if (indirectRewardsChanged || respectValueChanged) { console.log('RW: Indirect rewards settings changed, refreshing display'); refreshRewardDisplay(); } console.log('RW: Settings saved successfully'); // Close panel panel.style.maxHeight = '0'; panel.style.padding = '0 15px'; setTimeout(function() { panel.remove(); }, 300); // Refresh the reward display to update info box visibility and prices console.log('RW: Refreshing display after settings panel close'); refreshRewardDisplay(); } catch (error) { console.error('RW: Error saving settings:', error); alert('Error saving settings: ' + error.message); } }; // Assemble panel panel.appendChild(header); panel.appendChild(apiSection); panel.appendChild(otherSection); panel.appendChild(saveBtn); // Insert after the title header titleHeader.insertAdjacentElement('afterend', panel); } // Create trader selector dropdown function createTraderSelector() { const colors = getThemeColors(); const mobile = isMobile(); // Filter to only show custom trader names (not default "Trader X") const customTraders = sellerData.sellers .map((seller, index) => ({ ...seller, index })) .filter(seller => !seller.name.match(/^Trader \d+$/)); const container = document.createElement('div'); container.style.display = 'flex'; container.style.justifyContent = 'flex-end'; container.style.alignItems = 'center'; container.style.marginBottom = '10px'; container.style.fontSize = '11px'; container.style.gap = '6px'; container.style.width = '100%'; container.style.textAlign = 'right'; if (customTraders.length > 0) { // Create dropdown with custom traders const label = document.createElement('span'); label.textContent = 'Selected Trader:'; label.style.cssText = `color: ${colors.textMuted}; font-weight: normal; margin-right: 4px;`; const dropdown = document.createElement('select'); dropdown.style.cssText = ` background: ${colors.inputBg}; color: ${colors.textPrimary}; border: 1px solid ${colors.inputBorder}; border-radius: 3px; padding: 2px 6px; font-size: 11px; cursor: pointer; `; // Add options for each custom trader customTraders.forEach(trader => { const option = document.createElement('option'); option.value = trader.index; option.textContent = trader.name; option.selected = trader.index === sellerData.activeSeller; dropdown.appendChild(option); }); // Handle dropdown changes dropdown.addEventListener('change', function() { const newActiveIndex = parseInt(this.value); sellerData.activeSeller = newActiveIndex; saveSettings(); refreshRewardDisplay(); }); container.appendChild(label); container.appendChild(dropdown); // Add profile link if current trader has one const currentTrader = sellerData.sellers[sellerData.activeSeller]; if (currentTrader) { const userInfo = extractUserInfo(currentTrader.name); if (userInfo.hasId) { const profileLink = createProfileLink(userInfo.id, colors); profileLink.style.fontSize = '10px'; profileLink.style.marginLeft = '4px'; container.appendChild(profileLink); } } } else { // Show fallback message with clickable link const fallbackText = document.createElement('span'); fallbackText.style.cssText = `color: ${colors.textMuted};`; fallbackText.textContent = 'No custom prices - '; const configLink = document.createElement('a'); configLink.textContent = 'Configure Price List'; configLink.style.cssText = ` color: ${colors.primary}; text-decoration: none; cursor: pointer; `; configLink.addEventListener('click', function(e) { e.preventDefault(); showPricePanel(); }); fallbackText.appendChild(configLink); container.appendChild(fallbackText); } return container; } // Separate function to create grand total container function createGrandTotalContainer(grandTotalValue) { const colors = getThemeColors(); const mobile = isMobile(); // Pre-calculate API values to avoid scope issues let combinedApiTotal = 0; let showMobileApiComparison = false; let mobileApiHtml = ''; if (SETTINGS.showApiValues && rewardData && rewardData.length === 2) { combinedApiTotal = (rewardData[0].apiTotalValue || 0) + (rewardData[1].apiTotalValue || 0); if (combinedApiTotal > 0) { const percentDiff = grandTotalValue > 0 ? ((grandTotalValue - combinedApiTotal) / combinedApiTotal * 100) : 0; if (Math.abs(percentDiff) > 0.1) { showMobileApiComparison = true; let arrow = ''; let arrowColor = colors.textMuted; if (percentDiff > 0) { arrow = ' ' + getMobileArrow(true) + ' '; arrowColor = colors.success; } else { arrow = ' ' + getMobileArrow(false) + ' '; arrowColor = colors.danger; } mobileApiHtml = '<br><span style="font-size: 11px; color: ' + colors.textMuted + '; font-weight: normal;"><span style="color: ' + arrowColor + ';">' + arrow + Math.abs(percentDiff).toFixed(1) + '%</span> Market value ' + numberFormatter(combinedApiTotal, 2) + '</span>'; } } } // Create grand total container const grandContainer = document.createElement('div'); grandContainer.style.background = colors.panelBg; grandContainer.style.border = '1px solid ' + colors.panelBorder; grandContainer.style.borderRadius = '5px'; grandContainer.style.margin = mobile ? '5px 0' : '10px 0'; grandContainer.style.fontFamily = '"Segoe UI", Tahoma, Geneva, Verdana, sans-serif'; grandContainer.style.color = colors.textPrimary; grandContainer.style.boxShadow = colors.statBoxShadow; grandContainer.style.overflow = 'hidden'; grandContainer.style.position = 'relative'; // Create single header const header = document.createElement('div'); header.style.padding = mobile ? '10px 12px' : '12px 15px'; header.style.cursor = 'pointer'; header.style.userSelect = 'none'; header.style.background = colors.statBoxBg; header.style.borderLeft = '3px solid ' + colors.primary; // Blue left border header.style.transition = 'background-color 0.2s ease'; // Create header content container const headerContent = document.createElement('div'); const currentSeller = sellerData.sellers[sellerData.activeSeller]; const userInfo = extractUserInfo(currentSeller.name); if (mobile) { // Mobile: Clean layout with buyer info on third line headerContent.style.cssText = 'display: flex; justify-content: space-between; align-items: center; width: 100%;'; const titleSpan = document.createElement('span'); titleSpan.style.fontWeight = 'bold'; titleSpan.style.fontSize = '14px'; titleSpan.style.color = colors.textPrimary; titleSpan.textContent = 'Total Rewards'; // Create right-aligned content area with just blue value (clean first line) const rightContent = document.createElement('div'); rightContent.style.cssText = 'display: flex; align-items: center; gap: 6px;'; const blueValue = document.createElement('span'); blueValue.style.color = colors.primary; blueValue.style.fontWeight = 'bold'; blueValue.style.fontSize = '14px'; blueValue.textContent = grandTotalValue > 0 ? numberFormatter(grandTotalValue, 2) : '?'; // Conditional arrow for alignment consistency with faction panels const expandArrow = document.createElement('span'); expandArrow.id = 'grand-total-expand-icon'; if (SETTINGS.showIndirectRewards) { // Visible, functional arrow expandArrow.style.cssText = 'transition: transform 0.2s ease; font-size: 12px; color: ' + colors.primary + '; font-weight: bold; width: 12px; text-align: center; display: inline-block; cursor: pointer;'; } else { // Hidden arrow for layout consistency expandArrow.style.cssText = 'transition: transform 0.2s ease; font-size: 12px; color: ' + colors.primary + '; visibility: hidden; font-weight: bold; width: 12px; text-align: center; display: inline-block;'; } expandArrow.innerHTML = getExpandArrow(false); rightContent.appendChild(blueValue); rightContent.appendChild(expandArrow); headerContent.appendChild(titleSpan); headerContent.appendChild(rightContent); // Apply desktop wrapper pattern for mobile header.style.display = 'flex'; header.style.justifyContent = 'space-between'; header.style.alignItems = 'flex-start'; // Allow for multi-line content // Always use wrapper pattern for mobile to add buyer info on third line const headerWrapper = document.createElement('div'); headerWrapper.style.cssText = 'display: flex; flex-direction: column; align-items: flex-start;'; // Move title to wrapper headerWrapper.appendChild(headerContent); // Add API comparison line if needed (second line) if (SETTINGS.showApiValues && combinedApiTotal > 0) { const percentDiff = grandTotalValue > 0 ? ((grandTotalValue - combinedApiTotal) / combinedApiTotal * 100) : 0; if (Math.abs(percentDiff) > 0.1) { const apiLine = document.createElement('div'); apiLine.style.cssText = 'margin-top: 4px; font-size: 11px; color: ' + colors.textMuted + '; font-weight: normal;'; let arrow = ''; let arrowColor = colors.textMuted; if (percentDiff > 0) { arrow = ' ' + getMobileArrow(true) + ' '; arrowColor = colors.success; } else { arrow = ' ' + getMobileArrow(false) + ' '; arrowColor = colors.danger; } const apiSpan = document.createElement('span'); apiSpan.style.cssText = 'font-size: 11px; color: ' + colors.textMuted + '; font-weight: normal;'; apiSpan.innerHTML = '<span style="color: ' + arrowColor + ';">' + arrow + Math.abs(percentDiff).toFixed(1) + '%</span> Market value ' + numberFormatter(combinedApiTotal, 2); apiLine.appendChild(apiSpan); headerWrapper.appendChild(apiLine); } } // Add buyer info line (third line) - consistent spacing and handle missing buyer const buyerLine = document.createElement('div'); buyerLine.style.cssText = 'margin-top: 4px; font-size: 11px; color: ' + colors.textMuted + '; font-weight: normal;'; // Buyer info removed - now handled by trader selector above header.appendChild(headerWrapper); header.appendChild(rightContent); } else { // Desktop: Apply new layout matching faction headers const titleSpan = document.createElement('span'); titleSpan.style.fontWeight = 'bold'; titleSpan.style.fontSize = '16px'; titleSpan.style.color = colors.textPrimary; titleSpan.style.marginRight = '8px'; titleSpan.textContent = 'Total Rewards'; headerContent.appendChild(titleSpan); // Create right-aligned content area for buyer info and value const rightContent = document.createElement('div'); rightContent.style.cssText = 'display: flex; align-items: center; gap: 8px; flex: 1; justify-content: flex-end;'; // Buyer info removed - now handled by trader selector above // Add blue total value const valueSpan = document.createElement('span'); valueSpan.style.color = colors.primary; valueSpan.style.fontWeight = 'bold'; valueSpan.style.fontSize = '16px'; valueSpan.style.marginRight = '8px'; // FIXED: Add gap before invisible arrow to match faction headers valueSpan.textContent = grandTotalValue > 0 ? numberFormatter(grandTotalValue, 2) : '?'; // Conditional arrow for desktop layout const desktopExpandArrow = document.createElement('span'); desktopExpandArrow.id = 'grand-total-expand-icon-desktop'; desktopExpandArrow.style.transition = 'transform 0.2s ease'; desktopExpandArrow.style.fontSize = '14px'; desktopExpandArrow.style.color = colors.primary; desktopExpandArrow.style.fontWeight = 'bold'; desktopExpandArrow.style.width = '14px'; desktopExpandArrow.style.textAlign = 'center'; desktopExpandArrow.style.display = 'inline-block'; if (SETTINGS.showIndirectRewards) { // Visible, functional arrow desktopExpandArrow.style.cursor = 'pointer'; } else { // Hidden but maintains layout space desktopExpandArrow.style.visibility = 'hidden'; } desktopExpandArrow.innerHTML = getExpandArrow(false); rightContent.appendChild(valueSpan); rightContent.appendChild(desktopExpandArrow); // Set up flex layout header.style.display = 'flex'; header.style.justifyContent = 'space-between'; header.style.alignItems = 'flex-start'; // Allow for multi-line content // Handle API comparison on second line if enabled if (SETTINGS.showApiValues && combinedApiTotal > 0) { const percentDiff = grandTotalValue > 0 ? ((grandTotalValue - combinedApiTotal) / combinedApiTotal * 100) : 0; if (Math.abs(percentDiff) > 0.1) { // Create container for header content with API line const headerWrapper = document.createElement('div'); headerWrapper.style.cssText = 'display: flex; flex-direction: column; align-items: flex-start;'; // Move title to wrapper headerWrapper.appendChild(headerContent); // Add API comparison on second line const apiLine = document.createElement('div'); apiLine.style.cssText = 'margin-top: 4px; font-size: 12px; color: ' + colors.textMuted + '; font-weight: normal;'; let arrow = ''; let arrowColor = colors.textMuted; if (percentDiff > 0) { arrow = ' ' + getMobileArrow(true) + ' '; arrowColor = colors.success; } else { arrow = ' ' + getMobileArrow(false) + ' '; arrowColor = colors.danger; } apiLine.innerHTML = '<span style="color: ' + arrowColor + ';">' + arrow + Math.abs(percentDiff).toFixed(1) + '%</span> Market value ' + numberFormatter(combinedApiTotal, 2); headerWrapper.appendChild(apiLine); header.appendChild(headerWrapper); header.appendChild(rightContent); } else { // No API comparison, simpler layout header.appendChild(headerContent); header.appendChild(rightContent); } } else { // No API values, simpler layout header.appendChild(headerContent); header.appendChild(rightContent); } } // Create details section for indirect rewards (only if enabled) if (SETTINGS.showIndirectRewards) { const details = document.createElement('div'); details.id = 'grand-total-details'; details.style.display = 'none'; details.style.padding = mobile ? '12px 12px 8px 12px' : '12px 15px 8px 15px'; details.style.background = colors.panelBg; details.style.borderLeft = '3px solid ' + colors.primary; // Add click handler to header for expansion header.addEventListener('click', function() { toggleGrandTotalExpansion(); }); grandContainer.appendChild(header); grandContainer.appendChild(details); } else { grandContainer.appendChild(header); } return grandContainer; } function populateIndirectRewards() { const details = document.getElementById('grand-total-details'); if (!details || !SETTINGS.showIndirectRewards || !rewardData || rewardData.length !== 2) { return; } // Clear existing content details.innerHTML = ''; const colors = getThemeColors(); const mobile = isMobile(); // Create title for the section const sectionTitle = document.createElement('div'); sectionTitle.style.cssText = 'margin-bottom: 12px; color: ' + colors.textPrimary + '; font-size: 12px;'; sectionTitle.textContent = 'Other Rewards'; details.appendChild(sectionTitle); // Calculate and display indirect rewards for each faction for (let i = 0; i < 2; i++) { const faction = rewardData[i]; if (!faction) continue; // Create faction container const factionDiv = document.createElement('div'); factionDiv.style.cssText = 'background: ' + colors.statBoxBg + '; padding: ' + (mobile ? '8px' : '10px') + '; border-radius: 3px; margin-bottom: 8px; border-left: 3px solid ' + colors.primary + '; border: 1px solid ' + colors.statBoxBorder + '; box-shadow: ' + colors.statBoxShadow + ';'; // Faction name const factionName = document.createElement('div'); factionName.style.cssText = 'font-weight: bold; margin-bottom: 6px; color: ' + colors.textPrimary + '; font-size: ' + (mobile ? '12px' : '13px') + ';'; factionName.textContent = faction.factionName; factionDiv.appendChild(factionName); let indirectTotal = 0; // Respect rewards if (faction.respectAmount > 0) { const respectValue = faction.respectAmount * SETTINGS.respectValue; indirectTotal += respectValue; const respectRow = document.createElement('div'); respectRow.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; font-size: ' + (mobile ? '11px' : '12px') + ';'; const respectLabel = document.createElement('span'); respectLabel.style.color = colors.textSecondary; respectLabel.textContent = formatNumberWithCommas(faction.respectAmount) + ' respect x' + formatNumberWithCommas(SETTINGS.respectValue); const respectValueSpan = document.createElement('span'); respectValueSpan.style.cssText = 'color: ' + colors.primary + '; font-weight: bold;'; respectValueSpan.textContent = numberFormatter(respectValue, 2); respectRow.appendChild(respectLabel); respectRow.appendChild(respectValueSpan); factionDiv.appendChild(respectRow); } // Points rewards (extract from items) if (rawRewardData[i] && rawRewardData[i].items) { const pointsItems = rawRewardData[i].items.filter(item => item.type === 'points'); for (const pointsItem of pointsItems) { if (pointsItem.quantity > 0) { const pointsValue = pointsItem.quantity * pointsItem.pointValue; indirectTotal += pointsValue; const pointsRow = document.createElement('div'); pointsRow.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; font-size: ' + (mobile ? '11px' : '12px') + ';'; const pointsLabel = document.createElement('span'); pointsLabel.style.color = colors.textSecondary; pointsLabel.textContent = formatNumberWithCommas(pointsItem.quantity) + ' points x' + formatNumberWithCommas(pointsItem.pointValue); const pointsValueSpan = document.createElement('span'); pointsValueSpan.style.cssText = 'color: ' + colors.primary + '; font-weight: bold;'; pointsValueSpan.textContent = numberFormatter(pointsValue, 2); pointsRow.appendChild(pointsLabel); pointsRow.appendChild(pointsValueSpan); factionDiv.appendChild(pointsRow); } } } // Faction total if (indirectTotal > 0) { const totalRow = document.createElement('div'); totalRow.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-top: 8px; padding-top: 6px; border-top: 1px solid ' + colors.statBoxBorder + '; font-weight: bold; font-size: ' + (mobile ? '12px' : '13px') + ';'; const totalLabel = document.createElement('span'); totalLabel.style.color = colors.textPrimary; totalLabel.textContent = 'Faction Total'; const totalValueSpan = document.createElement('span'); totalValueSpan.style.cssText = 'color: ' + colors.success + '; font-weight: bold;'; totalValueSpan.textContent = numberFormatter(indirectTotal, 2); totalRow.appendChild(totalLabel); totalRow.appendChild(totalValueSpan); factionDiv.appendChild(totalRow); } else { // No indirect rewards const noRewardsDiv = document.createElement('div'); noRewardsDiv.style.cssText = 'font-style: italic; color: ' + colors.textMuted + '; font-size: ' + (mobile ? '11px' : '12px') + ';'; noRewardsDiv.textContent = 'No other rewards'; factionDiv.appendChild(noRewardsDiv); } details.appendChild(factionDiv); } } function toggleGrandTotalExpansion() { const details = document.getElementById('grand-total-details'); const mobileIcon = document.getElementById('grand-total-expand-icon'); const desktopIcon = document.getElementById('grand-total-expand-icon-desktop'); if (details && details.style.display === 'none') { details.style.display = 'block'; // Populate content when expanding populateIndirectRewards(); if (mobileIcon) { mobileIcon.style.transform = 'rotate(180deg)'; mobileIcon.innerHTML = getExpandArrow(true); } if (desktopIcon) { desktopIcon.style.transform = 'rotate(180deg)'; desktopIcon.innerHTML = getExpandArrow(true); } } else if (details) { details.style.display = 'none'; if (mobileIcon) { mobileIcon.style.transform = 'rotate(0deg)'; mobileIcon.innerHTML = getExpandArrow(false); } if (desktopIcon) { desktopIcon.style.transform = 'rotate(0deg)'; desktopIcon.innerHTML = getExpandArrow(false); } } } function createCompactContainer(totalValue, index, grandTotalValue, factionName, allRewardData) { const colors = getThemeColors(); const mobile = isMobile(); const container = document.createElement('div'); // Calculate distribution percentage for this reward const percentage = grandTotalValue > 0 ? (totalValue / grandTotalValue * 100).toFixed(1) : '?'; // Determine winner/loser and set border color with progressive fallback let borderColor; let isWinner = false; if (percentage !== '?' && parseFloat(percentage) > 50) { // Primary method: Use percentage-based determination when prices available isWinner = true; borderColor = colors.success; } else if (percentage !== '?' && parseFloat(percentage) <= 50) { // Primary method: Loser based on percentage isWinner = false; borderColor = colors.danger; } else if (allRewardData && allRewardData.length === 2) { // Secondary method: Use ranking-based determination when no prices const faction0Outcome = allRewardData[0].rankingOutcome; const faction1Outcome = allRewardData[1].rankingOutcome; console.log("RWAwardValue: Ranking outcomes - Faction 0:", faction0Outcome, "Faction 1:", faction1Outcome); // If one faction ranked down, they're the loser if (faction0Outcome === 'loser' && faction1Outcome !== 'loser') { // Faction 0 is loser, faction 1 is winner isWinner = index === 1; borderColor = index === 1 ? colors.success : colors.danger; console.log("RWAwardValue: Faction 0 lost, faction 1 won. Current index:", index, "isWinner:", isWinner); } else if (faction1Outcome === 'loser' && faction0Outcome !== 'loser') { // Faction 1 is loser, faction 0 is winner isWinner = index === 0; borderColor = index === 0 ? colors.success : colors.danger; console.log("RWAwardValue: Faction 1 lost, faction 0 won. Current index:", index, "isWinner:", isWinner); } else { // Tertiary fallback: Cannot determine winner/loser borderColor = colors.primary; // Use blue as neutral console.log("RWAwardValue: Cannot determine winner/loser from ranking, using blue"); } } else { // Tertiary fallback: Cannot determine winner/loser borderColor = colors.primary; // Use blue as neutral } // Set container styles container.style.background = colors.panelBg; container.style.border = '1px solid ' + colors.panelBorder; container.style.borderRadius = '5px'; container.style.margin = mobile ? '5px 0' : '10px 0'; container.style.fontFamily = '"Segoe UI", Tahoma, Geneva, Verdana, sans-serif'; container.style.color = colors.textPrimary; container.style.boxShadow = colors.statBoxShadow; container.style.overflow = 'hidden'; container.style.position = 'relative'; // Create header const header = document.createElement('div'); header.id = 'rw-reward-' + index; header.style.padding = mobile ? '10px 12px' : '12px 15px'; header.style.cursor = 'pointer'; header.style.userSelect = 'none'; header.style.display = 'flex'; header.style.justifyContent = 'space-between'; header.style.alignItems = 'center'; header.style.background = colors.statBoxBg; header.style.borderLeft = '3px solid ' + borderColor; header.style.borderBottom = '1px solid ' + colors.statBoxBorder; header.style.transition = 'background-color 0.2s ease'; // Create header content const headerContent = document.createElement('div'); const titleSpan = document.createElement('span'); titleSpan.style.fontWeight = 'bold'; titleSpan.style.fontSize = mobile ? '14px' : '16px'; titleSpan.style.color = colors.textPrimary; titleSpan.style.marginRight = '8px'; if (mobile) { // Mobile: working structure headerContent.style.cssText = 'display: flex; justify-content: space-between; align-items: center; width: 100%;'; const titleSpan = document.createElement('span'); titleSpan.style.fontWeight = 'bold'; titleSpan.style.fontSize = '14px'; titleSpan.style.color = colors.textPrimary; // Truncate faction name on mobile if longer than 18 characters const displayName = factionName.length > 18 ? factionName.substring(0, 18) + '...' : factionName; titleSpan.innerHTML = displayName + ' <span style="color: ' + borderColor + ';">(' + percentage + '%)</span>'; // Create right-aligned content area const rightContent = document.createElement('div'); rightContent.style.cssText = 'display: flex; align-items: center; gap: 6px;'; const blueValue = document.createElement('span'); blueValue.style.color = colors.primary; blueValue.style.fontWeight = 'bold'; blueValue.style.fontSize = '14px'; blueValue.textContent = totalValue > 0 ? numberFormatter(totalValue, 2) : '?'; const expandIcon = document.createElement('span'); expandIcon.id = 'expand-icon-' + index; expandIcon.style.cssText = 'transition: transform 0.2s ease; font-size: 12px; color: ' + colors.primary + '; font-weight: bold; width: 12px; text-align: center; display: inline-block;'; expandIcon.innerHTML = getExpandArrow(false); rightContent.appendChild(blueValue); rightContent.appendChild(expandIcon); headerContent.appendChild(titleSpan); headerContent.appendChild(rightContent); // Apply desktop wrapper pattern for mobile API comparison header.style.display = 'flex'; header.style.justifyContent = 'space-between'; header.style.alignItems = 'flex-start'; // Allow for multi-line content // Add API comparison using desktop wrapper pattern if (SETTINGS.showApiValues && rewardData[index] && rewardData[index].apiTotalValue > 0) { const apiValue = rewardData[index].apiTotalValue; const customValue = rewardData[index].totalValue; const percentDiff = customValue > 0 ? ((customValue - apiValue) / apiValue * 100) : 0; if (Math.abs(percentDiff) > 0.1) { // Create container for header content with API line const headerWrapper = document.createElement('div'); headerWrapper.style.cssText = 'display: flex; flex-direction: column; align-items: flex-start;'; // Move title to wrapper headerWrapper.appendChild(headerContent); // Add API comparison on second line const apiLine = document.createElement('div'); apiLine.style.cssText = 'margin-top: 4px; font-size: 11px; color: ' + colors.textMuted + '; font-weight: normal;'; let arrow = ''; let arrowColor = colors.textMuted; if (percentDiff > 0) { arrow = ' ' + getMobileArrow(true) + ' '; arrowColor = colors.success; } else { arrow = ' ' + getMobileArrow(false) + ' '; arrowColor = colors.danger; } apiLine.innerHTML = '<span style="color: ' + arrowColor + ';">' + arrow + Math.abs(percentDiff).toFixed(1) + '%</span> Market value ' + numberFormatter(apiValue, 2); headerWrapper.appendChild(apiLine); header.appendChild(headerWrapper); header.appendChild(rightContent); } else { // No API comparison, simpler layout header.appendChild(headerContent); header.appendChild(rightContent); } } else { // No API values, simpler layout header.appendChild(headerContent); header.appendChild(rightContent); } } else { // Desktop: Single line layout with percentage moved to left titleSpan.innerHTML = factionName + ' Rewards <span style="color: ' + borderColor + ';">(' + percentage + '%)</span>'; headerContent.appendChild(titleSpan); // Desktop: Create right-aligned content area for blue value only const rightContent = document.createElement('div'); rightContent.style.cssText = 'display: flex; align-items: center; gap: 8px; flex: 1; justify-content: flex-end;'; // Add blue total value (percentage now on left with title) const valueSpan = document.createElement('span'); valueSpan.style.color = colors.primary; valueSpan.style.fontWeight = 'bold'; valueSpan.style.fontSize = '16px'; valueSpan.style.marginRight = '8px'; // Gap before expand arrow valueSpan.textContent = totalValue > 0 ? numberFormatter(totalValue, 2) : '?'; rightContent.appendChild(valueSpan); // Create expand icon const expandIcon = document.createElement('span'); expandIcon.id = 'expand-icon-' + index; expandIcon.style.transition = 'transform 0.2s ease'; expandIcon.style.fontSize = '14px'; expandIcon.style.color = colors.primary; expandIcon.style.fontWeight = 'bold'; expandIcon.style.width = '14px'; expandIcon.style.textAlign = 'center'; expandIcon.style.display = 'inline-block'; expandIcon.innerHTML = getExpandArrow(false); // Start collapsed - USE innerHTML for HTML entities rightContent.appendChild(expandIcon); // Modify header to be flex container header.style.display = 'flex'; header.style.justifyContent = 'space-between'; header.style.alignItems = 'flex-start'; // Allow for multi-line content // Add API comparison on second line if enabled if (SETTINGS.showApiValues && rewardData[index] && rewardData[index].apiTotalValue > 0) { const apiValue = rewardData[index].apiTotalValue; const customValue = rewardData[index].totalValue; const percentDiff = customValue > 0 ? ((customValue - apiValue) / apiValue * 100) : 0; if (Math.abs(percentDiff) > 0.1) { // Create container for header content with API line const headerWrapper = document.createElement('div'); headerWrapper.style.cssText = 'display: flex; flex-direction: column; align-items: flex-start;'; // Move title to wrapper headerWrapper.appendChild(headerContent); // Add API comparison on second line const apiLine = document.createElement('div'); apiLine.style.cssText = 'margin-top: 4px; font-size: 12px; color: ' + colors.textMuted + '; font-weight: normal;'; let arrow = ''; let arrowColor = colors.textMuted; if (percentDiff > 0) { arrow = ' ' + getMobileArrow(true) + ' '; arrowColor = colors.success; } else { arrow = ' ' + getMobileArrow(false) + ' '; arrowColor = colors.danger; } apiLine.innerHTML = '<span style="color: ' + arrowColor + ';">' + arrow + Math.abs(percentDiff).toFixed(1) + '%</span> Market value ' + numberFormatter(apiValue, 2); headerWrapper.appendChild(apiLine); header.appendChild(headerWrapper); header.appendChild(rightContent); } else { // No API comparison, simpler layout header.appendChild(headerContent); header.appendChild(rightContent); } } else { // No API values, simpler layout header.appendChild(headerContent); header.appendChild(rightContent); } } // Create details section const details = document.createElement('div'); details.id = 'rw-details-' + index; details.style.display = 'none'; details.style.padding = mobile ? '12px 12px 8px 12px' : '12px 15px 8px 15px'; details.style.background = colors.panelBg; details.style.borderLeft = '3px solid ' + borderColor; container.appendChild(header); container.appendChild(details); return container; } function toggleExpansion(index) { const details = document.getElementById('rw-details-' + index); const icon = document.getElementById('expand-icon-' + index); if (details.style.display === 'none') { details.style.display = 'block'; icon.style.transform = 'rotate(180deg)'; icon.innerHTML = getExpandArrow(true); // USE innerHTML for HTML entities } else { details.style.display = 'none'; icon.style.transform = 'rotate(0deg)'; icon.innerHTML = getExpandArrow(false); // USE innerHTML for HTML entities } } async function valueRow(row, index) { console.log("RWAwardValue: Processing row", index); // Extract faction name and ranking outcome from the row text let factionName = "Unknown Faction"; let rankingOutcome = null; const rowText = row.innerText; const factionMatch = rowText.match(/^([A-Za-z0-9\.\s_'-]+)\s+(ranked\s+(up|down)\s+from|remained\s+at)/); if (factionMatch) { factionName = factionMatch[1].trim(); const rankingText = factionMatch[2].toLowerCase(); if (rankingText.includes('ranked down')) { rankingOutcome = 'loser'; } else if (rankingText.includes('ranked up') || rankingText.includes('remained at')) { rankingOutcome = 'winner'; } } console.log("RWAwardValue: Extracted faction name:", factionName, "ranking outcome:", rankingOutcome); // Extract respect amount before "bonus respect, " let respectAmount = 0; const respectMatch = row.innerText.match(/(\d{1,3}(?:,\d{3})*)\s+bonus respect,/); if (respectMatch) { respectAmount = parseInt(respectMatch[1].replace(/,/g, '')); console.log("RWAwardValue: Extracted respect amount:", respectAmount); } let startingIndex = row.innerText.indexOf("bonus respect, ") + 15; if (startingIndex === 14) { console.error("RWAwardValue: Could not find 'bonus respect, ' in row text"); rewardData[index] = { totalValue: 0, totalBB: 0, itemElements: [], factionName: factionName, respectAmount: respectAmount, row: row }; return; } let awardsString = row.innerText.substring(startingIndex, row.innerText.length); console.log("RWAwardValue: Awards string:", awardsString); let rowTotalValue = 0; let rawItems = []; // Store raw item data for recalculation const colors = getThemeColors(); const mobile = isMobile(); const items = awardsString.split(", "); let itemElements = []; for (const item of items) { console.log("RWAwardValue: Processing item:", item); let itemValue = 0; let itemBB = 0; let itemName = item; let rawItem = null; if (item.includes("points")) { const pointsAmount = parseInt(item.substring(0, item.indexOf(" ")).replace(",", "")); if (pointsAmount > 0) { let pointValueForCalc; if (API_KEY === 'YOUR_API_KEY_HERE' || !API_KEY) { pointValueForCalc = 31300; itemValue = 31300 * pointsAmount; console.log("RWAwardValue: Using mock point value"); } else { pointValueForCalc = await fetchPointValue(); itemValue = pointValueForCalc * pointsAmount; console.log("RWAwardValue: Using API point value:", pointValueForCalc); } rawItem = { type: 'points', quantity: pointsAmount, pointValue: pointValueForCalc }; rowTotalValue += itemValue; itemName = item + ' (' + numberFormatter(itemValue) + ' total)'; } else { console.log("RWAwardValue: Skipping 0 points"); continue; } } else if (item.includes("Cache")) { console.log("RWAwardValue: Found cache item:", item); let cacheValue = 0; let bbValue = 0; let cacheType = ''; let defaultValue = 0; if (item.includes("Armor Cache")) { cacheType = 'armorCache'; defaultValue = 312400000; } else if (item.includes("Heavy Arms Cache")) { cacheType = 'heavyArmsCache'; defaultValue = 250000000; } else if (item.includes("Medium Arms Cache")) { cacheType = 'mediumArmsCache'; defaultValue = 191000000; } else if (item.includes("Melee Cache")) { cacheType = 'meleeCache'; defaultValue = 139500000; } else if (item.includes("Small Arms Cache")) { cacheType = 'smallArmsCache'; defaultValue = 111400000; } const quantity = parseInt(item.substring(0, item.indexOf("x"))); // Determine price source based on settings and availability const customPrice = getCustomPrice(cacheType); const hasCustomPrice = customPrice > 0; const hasApiPrice = apiPriceCache.data && apiPriceCache.data[cacheType]; const hasValidApiKey = (API_KEY && API_KEY !== 'YOUR_API_KEY_HERE') || PDA_VALIDATED; // DEBUG: Log pricing decision console.log(`RW DEBUG: ${cacheType} - customPrice: ${customPrice}, hasCustomPrice: ${hasCustomPrice}, hasApiPrice: ${hasApiPrice}, showCustomPrices: ${SETTINGS.showCustomPrices}, hasValidApiKey: ${hasValidApiKey}`); if (hasApiPrice) { console.log(`RW DEBUG: ${cacheType} - API price available: ${apiPriceCache.data[cacheType]}`); } // Priority: Custom prices (if enabled) > API prices (if available) > No pricing (show "?") if (SETTINGS.showCustomPrices && hasCustomPrice) { cacheValue = customPrice; itemValue = cacheValue * quantity; console.log(`RW DEBUG: ${cacheType} - Using custom price: ${cacheValue}`); } else if (hasApiPrice && hasValidApiKey) { cacheValue = apiPriceCache.data[cacheType]; itemValue = cacheValue * quantity; console.log(`RW DEBUG: ${cacheType} - Using API price: ${cacheValue}`); } else { // No prices available - show "?" cacheValue = 0; itemValue = 0; console.log(`RW DEBUG: ${cacheType} - No pricing available, showing ?`); } rawItem = { type: 'cache', cacheType: cacheType, quantity: quantity, defaultValue: defaultValue }; rowTotalValue += itemValue; // Only format value if we have a price, otherwise show just the item name if (itemValue > 0) { itemName = item + ' (' + numberFormatter(itemValue) + ' total)'; } else { itemName = item; // Just show the item name without pricing } } if (rawItem) { rawItems.push(rawItem); } // Create item display element with ENHANCED FORMAT const itemDiv = document.createElement('div'); itemDiv.style.background = colors.statBoxBg; itemDiv.style.padding = mobile ? '8px' : '10px'; itemDiv.style.borderRadius = '3px'; itemDiv.style.marginBottom = '6px'; itemDiv.style.borderLeft = '3px solid ' + colors.primary; itemDiv.style.border = '1px solid ' + colors.statBoxBorder; itemDiv.style.boxShadow = colors.statBoxShadow; itemDiv.style.display = 'flex'; itemDiv.style.justifyContent = 'space-between'; itemDiv.style.alignItems = 'center'; itemDiv.style.fontSize = mobile ? '12px' : '13px'; const nameSpan = document.createElement('span'); nameSpan.style.color = colors.textSecondary; // NEW ENHANCED FORMAT for item names - USE CUSTOM PRICES let enhancedItemName = ''; let cacheTypeForApi = ''; if (item.includes("Cache")) { // Extract cache details for enhanced format const quantity = parseInt(item.substring(0, item.indexOf("x"))); let cacheTypeName = ''; let individualPrice = 0; if (item.includes("Armor Cache")) { cacheTypeName = 'Armor'; cacheTypeForApi = 'armorCache'; const customPrice = getCustomPrice('armorCache'); const hasCustomPrice = customPrice > 0; const hasApiPrice = SETTINGS.showApiValues && apiPriceCache.data && apiPriceCache.data['armorCache']; if (SETTINGS.showCustomPrices && hasCustomPrice) { individualPrice = customPrice; } else if (hasApiPrice) { individualPrice = apiPriceCache.data['armorCache']; } else { individualPrice = 0; } } else if (item.includes("Heavy Arms Cache")) { cacheTypeName = 'Heavy Arms'; cacheTypeForApi = 'heavyArmsCache'; const customPrice = getCustomPrice('heavyArmsCache'); const hasCustomPrice = customPrice > 0; const hasApiPrice = SETTINGS.showApiValues && apiPriceCache.data && apiPriceCache.data['heavyArmsCache']; if (SETTINGS.showCustomPrices && hasCustomPrice) { individualPrice = customPrice; } else if (hasApiPrice) { individualPrice = apiPriceCache.data['heavyArmsCache']; } else { individualPrice = 0; } } else if (item.includes("Medium Arms Cache")) { cacheTypeName = 'Medium Arms'; cacheTypeForApi = 'mediumArmsCache'; const customPrice = getCustomPrice('mediumArmsCache'); const hasCustomPrice = customPrice > 0; const hasApiPrice = SETTINGS.showApiValues && apiPriceCache.data && apiPriceCache.data['mediumArmsCache']; if (SETTINGS.showCustomPrices && hasCustomPrice) { individualPrice = customPrice; } else if (hasApiPrice) { individualPrice = apiPriceCache.data['mediumArmsCache']; } else { individualPrice = 0; } } else if (item.includes("Melee Cache")) { cacheTypeName = 'Melee'; cacheTypeForApi = 'meleeCache'; const customPrice = getCustomPrice('meleeCache'); const hasCustomPrice = customPrice > 0; const hasApiPrice = SETTINGS.showApiValues && apiPriceCache.data && apiPriceCache.data['meleeCache']; if (SETTINGS.showCustomPrices && hasCustomPrice) { individualPrice = customPrice; } else if (hasApiPrice) { individualPrice = apiPriceCache.data['meleeCache']; } else { individualPrice = 0; } } else if (item.includes("Small Arms Cache")) { cacheTypeName = 'Small Arms'; cacheTypeForApi = 'smallArmsCache'; const customPrice = getCustomPrice('smallArmsCache'); const hasCustomPrice = customPrice > 0; const hasApiPrice = SETTINGS.showApiValues && apiPriceCache.data && apiPriceCache.data['smallArmsCache']; if (SETTINGS.showCustomPrices && hasCustomPrice) { individualPrice = customPrice; } else if (hasApiPrice) { individualPrice = apiPriceCache.data['smallArmsCache']; } else { individualPrice = 0; } } if (mobile) { // Mobile: Simplified layout without quantity breakdown if (quantity === 1) { enhancedItemName = '1x ' + cacheTypeName + ' Cache'; } else { enhancedItemName = quantity + 'x ' + cacheTypeName + ' Cache'; } // Add API comparison if available and custom prices are enabled if (SETTINGS.showCustomPrices && SETTINGS.showApiValues && item.includes("Cache") && Object.keys(apiPriceCache.data).length > 0 && cacheTypeForApi && apiPriceCache.data[cacheTypeForApi] && individualPrice > 0) { const apiValue = apiPriceCache.data[cacheTypeForApi] * quantity; const customValue = itemValue; const percentDiff = customValue > 0 ? ((customValue - apiValue) / apiValue * 100) : 0; if (Math.abs(percentDiff) > 0.1) { let arrow = ''; let arrowColor = colors.textMuted; if (percentDiff > 0) { arrow = ' ' + getMobileArrow(true) + ' '; arrowColor = colors.success; } else { arrow = ' ' + getMobileArrow(false) + ' '; arrowColor = colors.danger; } enhancedItemName += '<br><span style="font-size: 11px; color: ' + colors.textMuted + ';"><span style="color: ' + arrowColor + ';">' + arrow + Math.abs(percentDiff).toFixed(1) + '%</span> Market value ' + numberFormatter(apiValue, 2) + '</span>'; } } } else { // Desktop: Single line layout if (individualPrice > 0) { if (quantity === 1) { enhancedItemName = '1x ' + cacheTypeName + ' Cache (' + formatNumberWithCommas(individualPrice) + ')'; } else { enhancedItemName = quantity + 'x ' + cacheTypeName + ' Cache (' + quantity + 'x ' + formatNumberWithCommas(individualPrice) + ')'; } } else { // No price available - show just quantity and cache name if (quantity === 1) { enhancedItemName = '1x ' + cacheTypeName + ' Cache'; } else { enhancedItemName = quantity + 'x ' + cacheTypeName + ' Cache'; } } } } else { // For points, use the existing format if (mobile) { enhancedItemName = itemName.replace(' (', '<br><span style="font-size: 11px; color: ' + colors.textMuted + ';">(') + '</span>'; } else { enhancedItemName = itemName; } cacheTypeForApi = 'points'; } nameSpan.innerHTML = enhancedItemName; // Use innerHTML for mobile line breaks const valueContainer = document.createElement('div'); valueContainer.style.display = 'flex'; valueContainer.style.alignItems = 'center'; valueContainer.style.gap = '8px'; // Add API comparison for cache items if enabled and API data available (DESKTOP ONLY) if (!mobile && SETTINGS.showCustomPrices && SETTINGS.showApiValues && item.includes("Cache") && Object.keys(apiPriceCache.data).length > 0 && cacheTypeForApi && apiPriceCache.data[cacheTypeForApi] && itemValue > 0) { const quantity = parseInt(item.substring(0, item.indexOf("x"))); const apiValue = apiPriceCache.data[cacheTypeForApi] * quantity; const customValue = itemValue; const percentDiff = customValue > 0 ? ((customValue - apiValue) / apiValue * 100) : 0; if (Math.abs(percentDiff) > 0.1) { // Only show if meaningful difference // Create wrapper for name with API line below const nameWrapper = document.createElement('div'); nameWrapper.style.cssText = 'display: flex; flex-direction: column; align-items: flex-start;'; // Create main item name span const mainNameSpan = document.createElement('span'); mainNameSpan.style.color = colors.textSecondary; mainNameSpan.innerHTML = enhancedItemName; // Create API comparison line with increased gap const apiLine = document.createElement('div'); apiLine.style.cssText = 'margin-top: 4px; font-size: 11px; color: ' + colors.textMuted + '; font-weight: normal;'; let arrow = ''; let arrowColor = colors.textMuted; if (percentDiff > 0) { arrow = getMobileArrow(true) + ' '; arrowColor = colors.success; } else { arrow = getMobileArrow(false) + ' '; arrowColor = colors.danger; } apiLine.innerHTML = '<span style="color: ' + arrowColor + ';">' + arrow + Math.abs(percentDiff).toFixed(1) + '%</span> Market value ' + numberFormatter(apiValue, 2); nameWrapper.appendChild(mainNameSpan); nameWrapper.appendChild(apiLine); // Adjust itemDiv to allow for multi-line content itemDiv.style.alignItems = 'flex-start'; itemDiv.appendChild(nameWrapper); } else { // No significant API difference, use normal layout nameSpan.innerHTML = enhancedItemName; itemDiv.appendChild(nameSpan); } } else { // No API comparison or mobile view, use normal layout nameSpan.innerHTML = enhancedItemName; itemDiv.appendChild(nameSpan); } const valueSpan = document.createElement('span'); valueSpan.style.color = colors.primary; valueSpan.style.fontWeight = 'bold'; valueSpan.textContent = numberFormatter(itemValue, 2); valueContainer.appendChild(valueSpan); // Only add valueContainer if we haven't already added nameWrapper if (!itemDiv.children.length) { itemDiv.appendChild(nameSpan); } itemDiv.appendChild(valueContainer); itemElements.push(itemDiv); // Store reference for updating during refresh if (rawItem) { itemDiv.setAttribute('data-cache-type', rawItem.cacheType || 'points'); itemDiv.setAttribute('data-item-type', rawItem.type); } } console.log("RWAwardValue: Row", index, "total value:", rowTotalValue); rewardData[index] = { totalValue: rowTotalValue, itemElements: itemElements, factionName: factionName, respectAmount: respectAmount, rankingOutcome: rankingOutcome, // Store ranking outcome for winner/loser determination row: row }; // Store raw data for recalculation rawRewardData[index] = { factionName: factionName, respectAmount: respectAmount, items: rawItems }; } // FIXED CSS SELECTOR - robust partial matching const checkRows = async function() { console.log("RWAwardValue: Checking for rows..."); // Try multiple selector strategies to find the reward rows let selectedElements = []; // Strategy 1: Use partial class name matching (most reliable) selectedElements = document.querySelectorAll("div[class*='memberBonusRow']"); // Strategy 2: If that fails, try other patterns if (selectedElements.length === 0) { selectedElements = document.querySelectorAll("div[class*='bonusRow']"); } // Strategy 3: Look for text patterns that indicate bonus rows if (selectedElements.length === 0) { const allDivs = document.querySelectorAll('div'); selectedElements = Array.from(allDivs).filter(div => { const text = div.innerText || ''; return text.includes('ranked up from') || text.includes('ranked down from'); }); } console.log("RWAwardValue: Found", selectedElements.length, "rows"); if (selectedElements.length > 0) { console.log("RWAwardValue: First element text:", selectedElements[0].innerText); } if (selectedElements.length == 2) { console.log("RWAwardValue: Found both rows, processing..."); clearInterval(timer); try { await valueRow(selectedElements[0], 0); await valueRow(selectedElements[1], 1); console.log("RWAwardValue: Data collected, creating containers..."); setTimeout(function() { createAndDisplayContainers(); }, 100); } catch (error) { console.error("RWAwardValue: Error processing rows:", error); } } }; // ======================================== // SCRIPT INITIALIZATION // ======================================== // Start checking for reward tables let timer = setInterval(checkRows, 200); // Start theme monitoring monitorThemeChanges(); console.log("RWAwardValue: Script setup complete v3.1"); })();