// ==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");
})();