您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Track plushies and flowers in Torn inventory and calculate missing items
// ==UserScript== // @name Torn Plushies & Flowers Tracker // @namespace http://tampermonkey.net/ // @version 1.4.2 // @description Track plushies and flowers in Torn inventory and calculate missing items // @author You // @match https://www.torn.com/item.php* // @match https://www.torn.com/preferences.php* // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @connect api.torn.com // @run-at document-idle // ==/UserScript== (() => { 'use strict'; // Enable debug logging const DEBUG = true; // Default configuration const DEFAULT_CONFIG = { apiKey: '', useMarketPrices: true, cacheDuration: 24, lastCacheUpdate: 0 }; // Load configuration from GM storage const loadConfig = () => { try { const savedConfig = GM_getValue('plushiesFlowersConfig'); return savedConfig ? JSON.parse(savedConfig) : DEFAULT_CONFIG; } catch (e) { log('Error loading configuration, using defaults', e); return DEFAULT_CONFIG; } }; // Save configuration to GM storage const saveConfig = (config) => { try { GM_setValue('plushiesFlowersConfig', JSON.stringify(config)); log('Configuration saved', config); } catch (e) { log('Error saving configuration', e); } }; // Load cached market prices const loadCachedPrices = () => { try { const cachedData = GM_getValue('plushiesFlowersPrices'); if (!cachedData) { log('No cached price data found'); return {}; } const parsedData = JSON.parse(cachedData); log('Loaded cached prices data:', parsedData); // Log the structure of the prices object for debugging if (parsedData.prices) { log(`Cache contains prices for ${Object.keys(parsedData.prices).length} items`); log('First few cached prices:', Object.entries(parsedData.prices).slice(0, 5)); } else { log('Cache does not contain a valid prices object'); } return parsedData; } catch (e) { log('Error loading cached prices, using empty cache', e); return {}; } }; // Save market prices to cache const saveCachedPrices = (prices) => { try { const cacheData = { timestamp: Date.now(), prices: prices }; GM_setValue('plushiesFlowersPrices', JSON.stringify(cacheData)); log('Market prices cached', cacheData); } catch (e) { log('Error caching market prices', e); } }; // Check if cache is valid (within cacheDuration) const isCacheValid = (config) => { try { const cachedData = GM_getValue('plushiesFlowersPrices'); if (!cachedData) return false; const data = JSON.parse(cachedData); const now = Date.now(); const cacheAge = (now - data.timestamp) / (1000 * 60 * 60); // hours return cacheAge < config.cacheDuration; } catch (e) { log('Error checking cache validity', e); return false; } }; // Clear the price cache const clearCache = () => { try { GM_setValue('plushiesFlowersPrices', ''); log('Price cache cleared'); } catch (e) { log('Error clearing cache', e); } }; // Current configuration let config = loadConfig(); // Helper function for logging const log = (message, data) => { if (DEBUG) { if (data) { console.log(`[Plushies & Flowers Tracker] ${message}`, data); } else { console.log(`[Plushies & Flowers Tracker] ${message}`); } } }; log('Script initialized'); // Configuration - Update these values if the total numbers change const TOTAL_PLUSHIES = 13; // Total unique plushies in a complete set const TOTAL_FLOWERS = 11; // Total unique flowers in a complete set // Plushie names for reference const PLUSHIE_NAMES = [ 'Teddy Bear', 'Camel', 'Chamois', 'Jaguar', 'Kitten', 'Lion', 'Monkey', 'Nessie', 'Panda', 'Red Fox', 'Sheep', 'Stingray', 'Wolverine' ]; // Flower names for reference const FLOWER_NAMES = [ 'Dahlia', 'Orchid', 'African Violet', 'Cherry Blossom', 'Peony', 'Ceibo Flower', 'Edelweiss', 'Crocus', 'Heather', 'Tribulus Omanense', 'Banana Orchid' ]; // Collections to store found items and prices const plushiesFound = new Map(); const flowersFound = new Map(); const plushiePrices = new Map(); const flowerPrices = new Map(); // Item IDs for plushies and flowers (used for both market links and images) const PLUSHIE_IDS = { 'Teddy Bear': 187, 'Camel': 384, 'Chamois': 273, 'Jaguar': 258, 'Kitten': 215, 'Lion': 281, 'Monkey': 269, 'Nessie': 266, 'Panda': 274, 'Red Fox': 268, 'Sheep': 186, 'Stingray': 618, 'Wolverine': 261 }; const FLOWER_IDS = { 'Dahlia': 260, 'Orchid': 264, 'African Violet': 282, 'Cherry Blossom': 277, 'Peony': 276, 'Ceibo Flower': 271, 'Edelweiss': 272, 'Crocus': 263, 'Heather': 267, 'Tribulus Omanense': 385, 'Banana Orchid': 617 }; // Function to create and add the tracker button const addTrackerButton = () => { // Only run on inventory pages if (!window.location.href.includes('item.php')) return; // Check if our container already exists (to avoid duplicates) if (document.getElementById('plushies-flowers-tracker')) { log('Tracker already exists, not adding again'); return; } // Create a container similar to the weapon ID script const container = document.createElement('div'); container.className = 'tutorial-cont'; container.id = 'plushies-flowers-tracker'; const titleContainer = document.createElement('div'); titleContainer.className = 'title-gray top-round'; titleContainer.setAttribute('role', 'heading'); titleContainer.setAttribute('aria-level', '5'); const title = document.createElement('span'); title.className = 'tutorial-title'; title.innerHTML = 'Plushies & Flowers Collection Tracker'; titleContainer.appendChild(title); container.appendChild(titleContainer); const description = document.createElement('div'); description.className = 'tutorial-desc bottom-round cont-gray p10'; description.innerHTML = ` <p>Track your plushies and flowers collections to see what you're missing!</p> <p>Make sure to scroll down completely on each page to load all items before analyzing.</p> `; const buttonWrapper = document.createElement('div'); buttonWrapper.style.display = 'flex'; buttonWrapper.style.justifyContent = 'space-around'; buttonWrapper.style.marginTop = '10px'; const plushiesButton = document.createElement('div'); plushiesButton.className = 'torn-btn'; plushiesButton.innerHTML = 'Analyze Plushies'; plushiesButton.style.width = '150px'; plushiesButton.style.display = 'flex'; plushiesButton.style.alignItems = 'center'; plushiesButton.style.justifyContent = 'center'; const flowersButton = document.createElement('div'); flowersButton.className = 'torn-btn'; flowersButton.innerHTML = 'Analyze Flowers'; flowersButton.style.width = '150px'; flowersButton.style.display = 'flex'; flowersButton.style.alignItems = 'center'; flowersButton.style.justifyContent = 'center'; buttonWrapper.appendChild(plushiesButton); buttonWrapper.appendChild(flowersButton); description.appendChild(buttonWrapper); container.appendChild(description); const delimiter = document.createElement('hr'); delimiter.className = 'delimiter-999 m-top10 m-bottom10'; // Find the last item list in the page to add our container after it // This ensures we don't add it multiple times and it's positioned correctly const itemLists = document.querySelectorAll('ul.items-cont'); const lastItemList = itemLists[itemLists.length - 1]; if (lastItemList && lastItemList.parentElement) { // Add some spacing const spacer = document.createElement('div'); spacer.style.height = '20px'; lastItemList.parentElement.insertAdjacentElement('afterend', spacer); spacer.insertAdjacentElement('afterend', container); } else { // Fallback to category-wrap if we can't find item lists const categoryWrap = document.getElementById('category-wrap'); if (categoryWrap) { categoryWrap.insertAdjacentElement('afterend', delimiter); categoryWrap.insertAdjacentElement('afterend', container); } } // Add click events plushiesButton.addEventListener('click', () => analyzePlushies()); flowersButton.addEventListener('click', () => analyzeFlowers()); }; // Function to directly scan the inventory for plushies and flowers const scanInventory = () => { log('Directly scanning inventory for plushies and flowers'); // Clear existing collections plushiesFound.clear(); flowersFound.clear(); log('Cleared existing collections'); }; // Function to scan plushies const scanPlushies = () => { log('Scanning plushies...'); // Find the plushies list const plushiesList = document.getElementById('plushies-items'); if (!plushiesList) { log('Plushies list not found'); return; } // Get all list items in the plushies section const plushieItems = Array.from(plushiesList.children); log(`Found ${plushieItems.length} plushie items`, plushieItems); // Process each plushie item plushieItems.forEach((item) => { processPlushieItem(item); }); }; // Function to scan flowers const scanFlowers = () => { log('Scanning flowers...'); // Find the flowers list const flowersList = document.getElementById('flowers-items'); if (!flowersList) { log('Flowers list not found'); return; } // Get all list items in the flowers section const flowerItems = Array.from(flowersList.children); log(`Found ${flowerItems.length} flower items`, flowerItems); // Process each flower item flowerItems.forEach((item) => { processFlowerItem(item); }); }; // Function to process a plushie item from the inventory const processPlushieItem = (item) => { var _a; try { // Extract the name from the item const nameElement = item.querySelector('.name'); if (!nameElement) return; // Get the name without 'Plushie' suffix let name = ((_a = nameElement.textContent) === null || _a === void 0 ? void 0 : _a.trim()) || ''; name = name.replace(' Plushie', ''); // Extract the quantity const quantityElement = item.querySelector('.qty'); const quantity = quantityElement ? parseInt(quantityElement.textContent || '0') : 1; // Extract the price const priceElement = item.querySelector('.price'); let price = 0; if (priceElement) { const priceText = priceElement.textContent || ''; price = parseInt(priceText.replace(/[^0-9]/g, '')) || 0; } // Add to collections plushiesFound.set(name, quantity); plushiePrices.set(name, price); log(`Found plushie: ${name}, Quantity: ${quantity}, Price: $${price}`); } catch (e) { log('Error processing plushie item', e); } }; // Function to process a flower item from the inventory const processFlowerItem = (item) => { var _a; try { // Extract the name from the item const nameElement = item.querySelector('.name'); if (!nameElement) return; // Get the name without 'Flower' suffix let name = ((_a = nameElement.textContent) === null || _a === void 0 ? void 0 : _a.trim()) || ''; name = name.replace(' Flower', ''); // Extract the quantity const quantityElement = item.querySelector('.qty'); const quantity = quantityElement ? parseInt(quantityElement.textContent || '0') : 1; // Extract the price const priceElement = item.querySelector('.price'); let price = 0; if (priceElement) { const priceText = priceElement.textContent || ''; price = parseInt(priceText.replace(/[^0-9]/g, '')) || 0; } // Add to collections flowersFound.set(name, quantity); flowerPrices.set(name, price); log(`Found flower: ${name}, Quantity: ${quantity}, Price: $${price}`); } catch (e) { log('Error processing flower item', e); } }; // Function to display a popup with results const showResultPopup = (content) => { // Remove any existing popup const existingPopup = document.getElementById('plushies-flowers-result'); if (existingPopup) { existingPopup.remove(); } // Create the popup container const popup = document.createElement('div'); popup.id = 'plushies-flowers-result'; popup.style.position = 'fixed'; popup.style.top = '50px'; popup.style.left = '50%'; popup.style.transform = 'translateX(-50%)'; popup.style.width = '800px'; popup.style.maxWidth = '90%'; popup.style.maxHeight = '80vh'; popup.style.backgroundColor = '#1a1a1a'; popup.style.border = '1px solid #444'; popup.style.borderRadius = '5px'; popup.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)'; popup.style.zIndex = '9999'; popup.style.overflow = 'hidden'; popup.style.display = 'flex'; popup.style.flexDirection = 'column'; // Create the header const header = document.createElement('div'); header.style.padding = '10px'; header.style.backgroundColor = '#333'; header.style.borderBottom = '1px solid #444'; header.style.display = 'flex'; header.style.justifyContent = 'space-between'; header.style.alignItems = 'center'; header.style.cursor = 'move'; const title = document.createElement('div'); title.textContent = 'Torn Plushies & Flowers Tracker'; title.style.fontWeight = 'bold'; title.style.color = '#ffb502'; const closeButton = document.createElement('div'); closeButton.textContent = '×'; closeButton.style.fontSize = '24px'; closeButton.style.color = '#fff'; closeButton.style.cursor = 'pointer'; closeButton.addEventListener('click', () => popup.remove()); header.appendChild(title); header.appendChild(closeButton); // Create the content area const contentArea = document.createElement('div'); contentArea.style.padding = '15px'; contentArea.style.overflowY = 'auto'; contentArea.style.maxHeight = 'calc(80vh - 50px)'; contentArea.innerHTML = content; popup.appendChild(header); popup.appendChild(contentArea); // Add to the page document.body.appendChild(popup); // Make the popup draggable let isDragging = false; let offsetX = 0; let offsetY = 0; header.addEventListener('mousedown', (e) => { isDragging = true; offsetX = e.clientX - popup.getBoundingClientRect().left; offsetY = e.clientY - popup.getBoundingClientRect().top; }); document.addEventListener('mousemove', (e) => { if (isDragging) { popup.style.left = (e.clientX - offsetX) + 'px'; popup.style.top = (e.clientY - offsetY) + 'px'; popup.style.transform = 'none'; } }); document.addEventListener('mouseup', () => { isDragging = false; }); }; // Function to display flowers results const displayFlowersResults = () => { // Check if we found any flowers log(`Found ${flowersFound.size} flowers in inventory`, Array.from(flowersFound.entries())); if (flowersFound.size === 0) { log('No flowers found, showing error popup'); showResultPopup('No flowers found in your inventory. Make sure you have clicked the Flowers tab and scrolled through your inventory to load all items.'); return; } // Count unique flowers const uniqueFlowersCount = flowersFound.size; // Calculate total flowers let totalFlowersCount = 0; flowersFound.forEach(qty => totalFlowersCount += qty); // Calculate missing flower types (unique flowers missing) const missingFlowerTypes = TOTAL_FLOWERS - uniqueFlowersCount; // Find the flower with the highest quantity to use as target for complete sets let maxQuantity = 0; flowersFound.forEach(qty => { if (qty > maxQuantity) maxQuantity = qty; }); // Default target sets is the maximum quantity (can be adjusted by user) let targetSets = maxQuantity; // Calculate total missing flowers count (how many flowers needed to reach potential maximum) let totalMissingFlowers = 0; // For each flower, calculate how many are needed to reach the target sets FLOWER_NAMES.forEach(name => { const quantity = flowersFound.get(name) || 0; const missing = targetSets - quantity; if (missing > 0) { totalMissingFlowers += missing; } }); // Prepare missing flowers list const missingFlowers = FLOWER_NAMES.filter(name => !flowersFound.has(name)); // Generate table rows for each flower let tableRows = ''; let totalInvestment = 0; // First add the flowers the user has FLOWER_NAMES.forEach(name => { const quantity = flowersFound.get(name) || 0; const missing = maxQuantity - quantity; const itemId = FLOWER_IDS[name] || 0; // Try to get price from API first, then fall back to DOM-extracted price let price = 0; if (config.useMarketPrices && config.apiKey) { price = getMarketPrice(itemId); } // If no API price, use DOM-extracted price if (price === 0) { price = flowerPrices.get(name) || 0; } log(`Price for ${name} Flower (ID: ${FLOWER_IDS[name]}): $${price}`); const totalPrice = price * missing; // Add to total investment if there are missing items if (missing > 0 && price > 0) { totalInvestment += totalPrice; } // Create market link const marketLink = `https://www.torn.com/page.php?sid=ItemMarket#/market/view=search&itemID=${itemId}`; // Use the item ID for the image const imageId = itemId; // Format price with commas const formattedPrice = price > 0 ? `$${price.toLocaleString()}` : '-'; const formattedTotalPrice = totalPrice > 0 ? `$${totalPrice.toLocaleString()}` : '-'; tableRows += ` <tr> <td style="vertical-align: middle; text-align: center;"><img src="https://www.torn.com/images/items/${imageId}/small.png" style="width: 30px; height: 30px; object-fit: contain;" alt="${name} Flower" /></td> <td style="vertical-align: middle; color: #fff;">${name} Flower</td> <td style="vertical-align: middle; text-align: center; color: #fff;">${quantity}</td> <td style="vertical-align: middle; text-align: center; color: #fff;">${missing}</td> <td style="vertical-align: middle; text-align: center; color: #fff;">${formattedPrice}</td> <td style="vertical-align: middle; text-align: center; color: #fff;">${formattedTotalPrice}</td> <td style="vertical-align: middle; text-align: center;">${missing > 0 ? `<a href="${marketLink}" target="_blank" class="t-blue">Buy</a>` : '-'}</td> </tr> `; }); // Add total row if (totalInvestment > 0) { tableRows += ` <tr> <td colspan="5" style="vertical-align: middle; text-align: right; color: #ffb502; font-weight: bold;">Total Investment:</td> <td style="vertical-align: middle; text-align: center; color: #ffb502; font-weight: bold;">$${totalInvestment.toLocaleString()}</td> <td></td> </tr> `; } // Calculate how many complete sets can be made const completeSets = uniqueFlowersCount < TOTAL_FLOWERS ? 0 : Math.min(...Array.from(flowersFound.values(), v => v || 0).filter(v => v > 0)); const potentialCompleteSets = maxQuantity; // Show results with table const resultMessage = ` <div style="color: #ffb502; font-size: 18px; font-weight: bold; margin-bottom: 10px;">Flowers Collection Progress</div> <div style="margin-bottom: 15px; padding: 10px; background-color: #222; border-radius: 5px;"> <p style="color: #fff; margin-bottom: 5px;">Target number of sets: <input type="number" id="flower-target-sets" value="${targetSets}" min="1" max="${maxQuantity}" style="width: 80px; padding: 5px; background-color: #333; color: #fff; border: 1px solid #555;"> <button id="update-flower-calc" style="padding: 5px 10px; background-color: #ffb502; color: #000; border: none; cursor: pointer;">Update</button></p> <p style="color: #aaa; font-size: 12px;">Adjust the target number of sets to calculate how many flowers you need to collect.</p> </div> <p style="color: #fff;">Unique flowers: ${uniqueFlowersCount}/${TOTAL_FLOWERS}</p> <p style="color: #fff;">Total flowers owned: ${totalFlowersCount}</p> <p style="color: #fff;">Complete sets: ${completeSets} (potential: ${potentialCompleteSets})</p> <p style="color: #fff;">Missing flower types: ${missingFlowerTypes}</p> <p style="color: #fff;">Total flowers needed: ${totalMissingFlowers}</p> <div style="height: 100%; overflow: auto; margin-top: 10px;"> <table class="torn-table" width="100%" style="border-collapse: collapse;"> <thead> <tr> <th style="padding: 8px; text-align: center; background-color: #333; color: #ffb502;"></th> <th style="padding: 8px; text-align: left; background-color: #333; color: #ffb502;">Name</th> <th style="padding: 8px; text-align: center; background-color: #333; color: #ffb502;">Owned</th> <th style="padding: 8px; text-align: center; background-color: #333; color: #ffb502;">Missing</th> <th style="padding: 8px; text-align: center; background-color: #333; color: #ffb502;">Unit Price</th> <th style="padding: 8px; text-align: center; background-color: #333; color: #ffb502;">Total</th> <th style="padding: 8px; text-align: center; background-color: #333; color: #ffb502;">Action</th> </tr> </thead> <tbody> ${tableRows} </tbody> </table> </div> `; showResultPopup(resultMessage); }; // Function to get market price for an item const getMarketPrice = (itemId) => { try { const cachedData = GM_getValue('plushiesFlowersPrices'); if (!cachedData) { log(`No cached price data found for item ${itemId}`); return 0; } const data = JSON.parse(cachedData); // Convert itemId to string since API returns string keys const itemIdStr = itemId.toString(); const price = data.prices && data.prices[itemIdStr] ? Number(data.prices[itemIdStr]) : 0; if (price > 0) { log(`Found cached price for item ${itemId}: $${price}`); } else { log(`No price found in cache for item ${itemId}`); } return price; } catch (e) { log('Error getting market price', e); return 0; } }; // Function to fetch market prices from Torn API v2 const fetchMarketPrices = (callback, forceUpdate = false) => { // Combine plushie and flower IDs for the API request const itemIds = []; // Add plushie IDs for (const name in PLUSHIE_IDS) { itemIds.push(PLUSHIE_IDS[name]); } // Add flower IDs for (const name in FLOWER_IDS) { itemIds.push(FLOWER_IDS[name]); } log(`Fetching market prices for ${itemIds.length} items...`); // Check if we have valid cached data and it's not too old const cachedData = loadCachedPrices(); if (!forceUpdate && isCacheValid(config)) { log('Using cached market prices'); callback(true); return; } // Build the API URL using the more efficient v2 torn/items endpoint // This endpoint provides just the item information we need with less data to process const apiUrl = `https://api.torn.com/v2/torn/items?ids=${itemIds.join(',')}&key=${config.apiKey}`; log(`Fetching item prices from API: ${apiUrl}`); // Make the API request GM_xmlhttpRequest({ method: 'GET', url: apiUrl, onload: (response) => { try { const data = JSON.parse(response.responseText); // Check for API errors if (data.error) { log('API Error:', data.error); callback(false); return; } // Check if we got a valid response if (!data.items) { log('Invalid API response - no items data:', data); callback(false); return; } // Process the items data const prices = {}; // Extract market values for each item for (const itemId in data.items) { const item = data.items[itemId]; // The market price is nested inside value.market_price if (item && item.id && item.value && item.value.market_price) { // Ensure we're storing numeric values as numbers const marketValue = Number(item.value.market_price); // Use the actual item ID from the API response const actualItemId = item.id.toString(); prices[actualItemId] = marketValue; log(`Fetched market value for item ${actualItemId} (${item.name}): $${marketValue.toLocaleString()}`); } else { log(`No market value found for item ${itemId} in API response`); } } // Log the total number of prices fetched log(`Fetched prices for ${Object.keys(prices).length} items out of ${itemIds.length} requested`); // Debug log the prices object before caching log('Prices object to be cached:', prices); // Cache the prices saveCachedPrices(prices); // Update the config with the last cache update time config.lastCacheUpdate = Date.now(); saveConfig(config); // Force a reload of the cached prices to verify they were stored correctly const verifiedCache = loadCachedPrices(); log('Verified cached prices after saving:', verifiedCache); callback(true); } catch (e) { log('Error processing API response', e); callback(false); } }, onerror: (error) => { log('API request error', error); callback(false); } }); }; // Function to analyze plushies const analyzePlushies = () => { log('Analyzing plushies...'); // Fetch market prices if needed if (config.useMarketPrices && config.apiKey) { fetchMarketPrices((success) => { if (success) { log('Market prices updated successfully'); } continueAnalyzePlushies(); }); } else { continueAnalyzePlushies(); } }; // Continue with plushie analysis after price fetch const continueAnalyzePlushies = () => { // Make sure we're on the plushies tab const plushiesTab = document.querySelector('a[data-category="plushies"]'); if (plushiesTab) { // Click the plushies tab to ensure items are loaded log('Clicking plushies tab to ensure items are loaded'); plushiesTab.click(); // Give a moment for the tab to load setTimeout(() => { // Scan for plushies directly scanPlushies(); displayPlushiesResults(); }, 500); } else { // Try to scan anyway scanPlushies(); displayPlushiesResults(); } }; // Function to display plushies results const displayPlushiesResults = () => { // Check if we found any plushies log(`Found ${plushiesFound.size} plushies in inventory`, Array.from(plushiesFound.entries())); if (plushiesFound.size === 0) { log('No plushies found, showing error popup'); showResultPopup('No plushies found in your inventory. Make sure you have clicked the Plushies tab and scrolled through your inventory to load all items.'); return; } // Count unique plushies const uniquePlushiesCount = plushiesFound.size; // Calculate total plushies let totalPlushiesCount = 0; plushiesFound.forEach(qty => totalPlushiesCount += qty); // Calculate missing plushies types (unique plushies missing) const missingPlushiesTypes = TOTAL_PLUSHIES - uniquePlushiesCount; // Find the plushie with the highest quantity to use as target for complete sets let maxQuantity = 0; plushiesFound.forEach(qty => { if (qty > maxQuantity) maxQuantity = qty; }); // Default target sets is the maximum quantity (can be adjusted by user) let targetSets = maxQuantity; // Calculate total missing plushies count (how many plushies needed to reach potential maximum) let totalMissingPlushies = 0; // For each plushie, calculate how many are needed to reach the target sets PLUSHIE_NAMES.forEach(name => { const quantity = plushiesFound.get(name) || 0; const missing = targetSets - quantity; if (missing > 0) { totalMissingPlushies += missing; } }); // Prepare missing plushies list const missingPlushies = PLUSHIE_NAMES.filter(name => !plushiesFound.has(name)); // Generate table rows for each plushie let tableRows = ''; let totalInvestment = 0; let singleSetValue = 0; // Track the market value of a single complete set // First add the plushies the user has PLUSHIE_NAMES.forEach(name => { const quantity = plushiesFound.get(name) || 0; const missing = maxQuantity - quantity; const itemId = PLUSHIE_IDS[name] || 0; // Try to get price from API first, then fall back to DOM-extracted price let price = 0; if (config.useMarketPrices && config.apiKey) { price = getMarketPrice(itemId); } // If no API price, use DOM-extracted price if (price === 0) { price = plushiePrices.get(name) || 0; } log(`Price for ${name} Plushie (ID: ${PLUSHIE_IDS[name]}): $${price}`); const totalPrice = price * missing; // Add to total investment if there are missing items if (missing > 0 && price > 0) { totalInvestment += totalPrice; } // Add to single set value (one of each plushie) if (price > 0) { singleSetValue += price; } // Create market link const marketLink = `https://www.torn.com/page.php?sid=ItemMarket#/market/view=search&itemID=${itemId}`; // Use the item ID for the image const imageId = itemId; // Format price with commas const formattedPrice = price > 0 ? `$${price.toLocaleString()}` : '-'; const formattedTotalPrice = totalPrice > 0 ? `$${totalPrice.toLocaleString()}` : '-'; tableRows += ` <tr> <td style="vertical-align: middle; text-align: center;"><img src="https://www.torn.com/images/items/${imageId}/small.png" style="width: 30px; height: 30px; object-fit: contain;" alt="${name} Plushie" /></td> <td style="vertical-align: middle; color: #fff;">${name} Plushie</td> <td style="vertical-align: middle; text-align: center; color: #fff;">${quantity}</td> <td style="vertical-align: middle; text-align: center; color: #fff;">${missing}</td> <td style="vertical-align: middle; text-align: center; color: #fff;">${formattedPrice}</td> <td style="vertical-align: middle; text-align: center; color: #fff;">${formattedTotalPrice}</td> <td style="vertical-align: middle; text-align: center;">${missing > 0 ? `<a href="${marketLink}" target="_blank" class="t-blue">Buy</a>` : '-'}</td> </tr> `; }); // Add total investment row if (totalInvestment > 0) { tableRows += ` <tr> <td colspan="5" style="vertical-align: middle; text-align: right; color: #ffb502; font-weight: bold;">Total Investment:</td> <td style="vertical-align: middle; text-align: center; color: #ffb502; font-weight: bold;">$${totalInvestment.toLocaleString()}</td> <td></td> </tr> `; } // Add single set value row if (singleSetValue > 0) { tableRows += ` <tr> <td colspan="5" style="vertical-align: middle; text-align: right; color: #ffb502; font-weight: bold;">Market Value of Single Set:</td> <td style="vertical-align: middle; text-align: center; color: #ffb502; font-weight: bold;">$${singleSetValue.toLocaleString()}</td> <td></td> </tr> `; } // Calculate how many complete sets can be made // If any plushie is missing (uniquePlushiesCount < TOTAL_PLUSHIES), then no complete sets can be made const completeSets = uniquePlushiesCount < TOTAL_PLUSHIES ? 0 : Math.min(...Array.from(plushiesFound.values(), v => v || 0).filter(v => v > 0)); const potentialCompleteSets = maxQuantity; // Show results with table const resultMessage = ` <div style="color: #ffb502; font-size: 18px; font-weight: bold; margin-bottom: 10px;">Plushies Collection Progress</div> <div style="margin-bottom: 15px; padding: 10px; background-color: #222; border-radius: 5px;"> <p style="color: #fff; margin-bottom: 5px;">Target number of sets: <input type="number" id="plushie-target-sets" value="${targetSets}" min="1" max="${maxQuantity}" style="width: 80px; padding: 5px; background-color: #333; color: #fff; border: 1px solid #555;"> <button id="update-plushie-calc" style="padding: 5px 10px; background-color: #ffb502; color: #000; border: none; cursor: pointer;">Update</button></p> <p style="color: #aaa; font-size: 12px;">Adjust the target number of sets to calculate how many plushies you need to collect.</p> </div> <div style="display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 15px; padding: 15px; background-color: #222; border-radius: 5px;"> <div style="flex: 1; min-width: 200px; background-color: #333; padding: 12px; border-radius: 5px; border-left: 3px solid #ffb502;"> <div style="color: #aaa; font-size: 12px; margin-bottom: 5px;">Unique Plushies</div> <div style="color: #fff; font-size: 16px; font-weight: bold;">${uniquePlushiesCount}/${TOTAL_PLUSHIES}</div> </div> <div style="flex: 1; min-width: 200px; background-color: #333; padding: 12px; border-radius: 5px; border-left: 3px solid #ffb502;"> <div style="color: #aaa; font-size: 12px; margin-bottom: 5px;">Total Plushies Owned</div> <div style="color: #fff; font-size: 16px; font-weight: bold;">${totalPlushiesCount.toLocaleString()}</div> </div> <div style="flex: 1; min-width: 200px; background-color: #333; padding: 12px; border-radius: 5px; border-left: 3px solid #ffb502;"> <div style="color: #aaa; font-size: 12px; margin-bottom: 5px;">Complete Sets</div> <div style="color: #fff; font-size: 16px; font-weight: bold;">${completeSets} <span style="color: #aaa; font-size: 12px;">(potential: ${potentialCompleteSets})</span></div> </div> <div style="flex: 1; min-width: 200px; background-color: #333; padding: 12px; border-radius: 5px; border-left: 3px solid #ffb502;"> <div style="color: #aaa; font-size: 12px; margin-bottom: 5px;">Missing Plushie Types</div> <div style="color: #fff; font-size: 16px; font-weight: bold;">${missingPlushiesTypes}</div> </div> <div style="flex: 1; min-width: 200px; background-color: #333; padding: 12px; border-radius: 5px; border-left: 3px solid #ffb502;"> <div style="color: #aaa; font-size: 12px; margin-bottom: 5px;">Total Plushies Needed</div> <div style="color: #fff; font-size: 16px; font-weight: bold;">${totalMissingPlushies.toLocaleString()}</div> </div> </div> <div style="height: 100%; overflow: auto; margin-top: 10px;"> <table class="torn-table" width="100%" style="border-collapse: collapse;"> <thead> <tr> <th style="padding: 8px; text-align: center; background-color: #333; color: #ffb502;"></th> <th style="padding: 8px; text-align: left; background-color: #333; color: #ffb502;">Name</th> <th style="padding: 8px; text-align: center; background-color: #333; color: #ffb502;">Owned</th> <th style="padding: 8px; text-align: center; background-color: #333; color: #ffb502;">Missing</th> <th style="padding: 8px; text-align: center; background-color: #333; color: #ffb502;">Unit Price</th> <th style="padding: 8px; text-align: center; background-color: #333; color: #ffb502;">Total</th> <th style="padding: 8px; text-align: center; background-color: #333; color: #ffb502;">Action</th> </tr> </thead> <tbody> ${tableRows} </tbody> </table> </div> <div style="margin-top: 20px; display: flex; justify-content: flex-end;"> <button id="export-plushies-data" style="padding: 10px 15px; background-color: #ffb502; color: #000; border: none; border-radius: 5px; cursor: pointer; font-weight: bold; display: flex; align-items: center;"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" style="margin-right: 8px; fill: currentColor;"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg> Export Missing Plushies </button> </div> `; showResultPopup(resultMessage); // Function to export missing plushies data const exportMissingPlushiesData = () => { // Create a data object with missing plushies const missingPlushiesData = { timestamp: new Date().toISOString(), targetSets: targetSets, totalMissingPlushies: totalMissingPlushies, totalInvestment: totalInvestment, items: [] }; // Add each missing plushie to the data PLUSHIE_NAMES.forEach(name => { const quantity = plushiesFound.get(name) || 0; const itemId = PLUSHIE_IDS[name] || 0; const price = getMarketPrice(itemId); const missing = Math.max(0, targetSets - quantity); if (missing > 0) { missingPlushiesData.items.push({ name: `${name} Plushie`, itemId: itemId, missing: missing, unitPrice: price, totalCost: missing * price }); } }); // Convert to JSON and create a downloadable file const dataStr = JSON.stringify(missingPlushiesData, null, 2); const dataUri = `data:application/json;charset=utf-8,${encodeURIComponent(dataStr)}`; // Create a temporary link element and trigger download const exportLink = document.createElement('a'); exportLink.setAttribute('href', dataUri); exportLink.setAttribute('download', `torn-plushies-shopping-list-${new Date().toISOString().split('T')[0]}.json`); document.body.appendChild(exportLink); exportLink.click(); document.body.removeChild(exportLink); // Also copy a simplified version to clipboard for easy sharing const clipboardText = missingPlushiesData.items.map(item => `${item.name}: ${item.missing} needed`).join('\n'); const clipboardHeader = `Torn Plushies Shopping List (${new Date().toLocaleDateString()})\n\n`; navigator.clipboard.writeText(clipboardHeader + clipboardText) .then(() => { alert('Shopping list exported! A JSON file has been downloaded and a text version copied to your clipboard.'); }) .catch(err => { console.error('Could not copy to clipboard:', err); alert('Shopping list exported! A JSON file has been downloaded.'); }); }; // Add event handlers for the buttons setTimeout(() => { // Export button event handler const exportButton = document.getElementById('export-plushies-data'); if (exportButton) { exportButton.addEventListener('click', exportMissingPlushiesData); } // Update button event handler const updateButton = document.getElementById('update-plushie-calc'); if (updateButton) { updateButton.addEventListener('click', () => { const targetSetsInput = document.getElementById('plushie-target-sets'); if (targetSetsInput) { const newTargetSets = parseInt(targetSetsInput.value, 10); if (!isNaN(newTargetSets) && newTargetSets > 0 && newTargetSets <= maxQuantity) { // Recalculate missing plushies with new target let newTotalMissingPlushies = 0; let newTableRows = ''; let newTotalInvestment = 0; let newSingleSetValue = 0; // Track the market value of a single complete set // Update the table rows with new calculations PLUSHIE_NAMES.forEach(name => { const quantity = plushiesFound.get(name) || 0; // Get the item ID for the plushie const itemId = PLUSHIE_IDS[name] || 0; // Get the market price using the item ID const price = getMarketPrice(itemId); const missing = Math.max(0, newTargetSets - quantity); if (missing > 0) { newTotalMissingPlushies += missing; const total = missing * price; newTotalInvestment += total; } // Add to single set value (one of each plushie) if (price > 0) { newSingleSetValue += price; } // Format price with commas const formattedPrice = price.toLocaleString(); const formattedTotal = (missing * price).toLocaleString(); newTableRows += ` <tr> <td style="vertical-align: middle; text-align: center;"><img src="https://www.torn.com/images/items/${itemId}/small.png" style="width: 30px; height: 30px; object-fit: contain;" alt="${name} Plushie" /></td> <td style="vertical-align: middle; color: #fff;">${name} Plushie</td> <td style="vertical-align: middle; text-align: center; color: #fff;">${quantity}</td> <td style="vertical-align: middle; text-align: center; color: #fff;">${missing}</td> <td style="vertical-align: middle; text-align: center; color: #fff;">$${formattedPrice}</td> <td style="vertical-align: middle; text-align: center; color: #fff;">${missing > 0 ? '$' + formattedTotal : '-'}</td> <td style="vertical-align: middle; text-align: center;">${missing > 0 ? `<a href="https://www.torn.com/page.php?sid=ItemMarket#/market/view=search&itemID=${itemId}" target="_blank" style="color: #ffb502; text-decoration: none;">Buy</a>` : ''}</td> </tr> `; }); // Create footer rows with the updated total investment and single set value const totalInvestmentRow = ` <tr> <td colspan="5" style="text-align: right; padding: 10px; color: #ffb502; font-weight: bold;">Total Investment:</td> <td style="text-align: center; padding: 10px; color: #ffb502; font-weight: bold;">$${newTotalInvestment.toLocaleString()}</td> <td></td> </tr> `; const singleSetValueRow = ` <tr> <td colspan="5" style="text-align: right; padding: 10px; color: #ffb502; font-weight: bold;">Market Value of Single Set:</td> <td style="text-align: center; padding: 10px; color: #ffb502; font-weight: bold;">$${newSingleSetValue.toLocaleString()}</td> <td></td> </tr> `; // Update the table with new rows including the footer rows const tableBody = document.querySelector('.torn-table tbody'); if (tableBody) { tableBody.innerHTML = newTableRows + totalInvestmentRow + singleSetValueRow; } // Update the summary information in the grid layout const summaryElements = document.querySelectorAll('div[style*="flex: 1"]'); summaryElements.forEach(element => { const labelElement = element.querySelector('div:first-child'); const valueElement = element.querySelector('div:last-child'); if (labelElement && valueElement) { const label = labelElement.textContent || ''; if (label.includes('Total Plushies Needed')) { valueElement.innerHTML = `${newTotalMissingPlushies.toLocaleString()}`; } else if (label.includes('Missing Plushie Types')) { valueElement.innerHTML = `${missingPlushiesTypes}`; } else if (label.includes('Complete Sets')) { valueElement.innerHTML = `${completeSets} <span style="color: #aaa; font-size: 12px;">(potential: ${potentialCompleteSets})</span>`; } } }); } } }); } }, 500); }; // Function to analyze flowers const analyzeFlowers = () => { log('Analyzing flowers...'); // Fetch market prices if needed if (config.useMarketPrices && config.apiKey) { fetchMarketPrices((success) => { if (success) { log('Market prices updated successfully'); } continueAnalyzeFlowers(); }); } else { continueAnalyzeFlowers(); } }; // Continue with flower analysis after price fetch const continueAnalyzeFlowers = () => { // Make sure we're on the flowers tab const flowersTab = document.querySelector('a[data-category="flowers"]'); if (flowersTab) { // Click the flowers tab to ensure items are loaded log('Clicking flowers tab to ensure items are loaded'); flowersTab.click(); // Give a moment for the tab to load setTimeout(() => { // Scan for flowers directly scanFlowers(); displayFlowersResults(); }, 500); } else { // Try to scan anyway scanFlowers(); displayFlowersResults(); } }; // Function to create the configuration UI const createConfigUI = () => { log('Creating configuration UI'); // Check if our settings panel already exists to prevent duplicates if (document.getElementById('plushies-flowers-settings')) { log('Settings panel already exists, not creating another one'); return; } // Find the preferences container const prefsContainer = document.querySelector('.preferences-container'); if (!prefsContainer) { log('Preferences container not found'); return; } // Create our config container const configContainer = document.createElement('div'); configContainer.className = 'preferences-container-wrap'; configContainer.id = 'plushies-flowers-settings'; // Create the title const titleDiv = document.createElement('div'); titleDiv.className = 'title-black top-round'; titleDiv.textContent = 'Plushies & Flowers Tracker Settings'; configContainer.appendChild(titleDiv); // Create the content container const content = document.createElement('div'); content.className = 'cont-gray bottom-round'; content.style.padding = '10px'; // Create the API key input const apiKeyLabel = document.createElement('label'); apiKeyLabel.textContent = 'Torn API Key (requires v2 access):'; apiKeyLabel.style.display = 'block'; apiKeyLabel.style.marginBottom = '5px'; content.appendChild(apiKeyLabel); const apiKeyContainer = document.createElement('div'); apiKeyContainer.style.display = 'flex'; apiKeyContainer.style.marginBottom = '15px'; apiKeyContainer.style.alignItems = 'center'; const apiKeyInput = document.createElement('input'); apiKeyInput.type = 'password'; apiKeyInput.value = config.apiKey; apiKeyInput.style.flex = '1'; apiKeyInput.style.marginRight = '10px'; apiKeyInput.style.padding = '5px'; apiKeyContainer.appendChild(apiKeyInput); const showApiKeyButton = document.createElement('button'); showApiKeyButton.className = 'torn-btn'; showApiKeyButton.textContent = 'Show API Key'; showApiKeyButton.addEventListener('click', () => { if (apiKeyInput.type === 'password') { apiKeyInput.type = 'text'; showApiKeyButton.textContent = 'Hide API Key'; } else { apiKeyInput.type = 'password'; showApiKeyButton.textContent = 'Show API Key'; } }); apiKeyContainer.appendChild(showApiKeyButton); content.appendChild(apiKeyContainer); // Create the use market prices checkbox const useMarketContainer = document.createElement('div'); useMarketContainer.style.marginBottom = '15px'; const useMarketCheck = document.createElement('input'); useMarketCheck.type = 'checkbox'; useMarketCheck.id = 'use-market-prices'; useMarketCheck.checked = config.useMarketPrices; useMarketContainer.appendChild(useMarketCheck); const useMarketLabel = document.createElement('label'); useMarketLabel.htmlFor = 'use-market-prices'; useMarketLabel.textContent = ' Use market prices from Torn API'; useMarketLabel.style.marginLeft = '5px'; useMarketContainer.appendChild(useMarketLabel); content.appendChild(useMarketContainer); // Create the cache duration input const cacheContainer = document.createElement('div'); cacheContainer.style.marginBottom = '15px'; const cacheLabel = document.createElement('label'); cacheLabel.textContent = 'Cache duration (hours): '; cacheContainer.appendChild(cacheLabel); const cacheInput = document.createElement('input'); cacheInput.type = 'number'; cacheInput.min = '1'; cacheInput.max = '72'; cacheInput.value = config.cacheDuration.toString(); cacheInput.style.width = '60px'; cacheInput.style.marginLeft = '5px'; cacheContainer.appendChild(cacheInput); content.appendChild(cacheContainer); const lastUpdateDiv = document.createElement('div'); lastUpdateDiv.style.marginBottom = '15px'; let lastUpdateText = 'Cache status: '; try { const cachedData = GM_getValue('plushiesFlowersPrices'); if (cachedData) { const data = JSON.parse(cachedData); const date = new Date(data.timestamp); lastUpdateText += `Last updated on ${date.toLocaleString()}`; } else { lastUpdateText += 'No cached data'; } } catch (e) { lastUpdateText += 'Error reading cache'; } lastUpdateDiv.textContent = lastUpdateText; content.appendChild(lastUpdateDiv); // Buttons row const buttonsDiv = document.createElement('div'); buttonsDiv.style.display = 'flex'; buttonsDiv.style.gap = '10px'; buttonsDiv.style.flexWrap = 'wrap'; // Save button const saveButton = document.createElement('button'); saveButton.className = 'torn-btn'; saveButton.textContent = 'Save Settings'; saveButton.addEventListener('click', () => { config.apiKey = apiKeyInput.value.trim(); config.useMarketPrices = useMarketCheck.checked; config.cacheDuration = parseInt(cacheInput.value) || 24; saveConfig(config); alert('Settings saved!'); }); buttonsDiv.appendChild(saveButton); // Clear cache button const clearButton = document.createElement('button'); clearButton.className = 'torn-btn'; clearButton.textContent = 'Clear Price Cache'; clearButton.addEventListener('click', () => { clearCache(); alert('Price cache cleared!'); // Update the last update text lastUpdateDiv.textContent = 'Cache status: No cached data (cleared)'; }); buttonsDiv.appendChild(clearButton); // Update prices button const updateButton = document.createElement('button'); updateButton.className = 'torn-btn'; updateButton.textContent = 'Update Prices Now'; updateButton.addEventListener('click', () => { // Check if API key is set if (!apiKeyInput.value.trim()) { alert('Please enter an API key first!'); return; } // Disable button during update updateButton.disabled = true; updateButton.textContent = 'Updating...'; // Save current settings first config.apiKey = apiKeyInput.value.trim(); config.useMarketPrices = useMarketCheck.checked; config.cacheDuration = parseInt(cacheInput.value) || 24; saveConfig(config); // Clear existing cache first log('Clearing existing price cache before update'); GM_setValue('plushiesFlowersPrices', ''); // Force fetch new prices with forceUpdate=true fetchMarketPrices((success) => { updateButton.disabled = false; updateButton.textContent = 'Update Prices Now'; if (success) { // Verify the cache was updated properly const cachedData = GM_getValue('plushiesFlowersPrices'); if (cachedData) { try { const parsedData = JSON.parse(cachedData); const priceCount = parsedData.prices ? Object.keys(parsedData.prices).length : 0; log(`Cache verification: Found ${priceCount} prices in cache`); alert(`Market prices updated successfully! Cached ${priceCount} item prices.`); } catch (e) { log('Error verifying cache after update', e); alert('Market prices updated but there may be an issue with the cache.'); } } else { log('No cache data found after update'); alert('Market prices update failed - no cache data found.'); } // Update the last update text try { const cachedData = GM_getValue('plushiesFlowersPrices'); if (cachedData) { const data = JSON.parse(cachedData); const date = new Date(data.timestamp); lastUpdateDiv.textContent = `Cache status: Last updated on ${date.toLocaleString()}`; } } catch (e) { log('Error updating cache status text', e); } } else { alert('Failed to update market prices. Make sure your API key has access to the market endpoint in API v2.'); } }, true); // Force update }); buttonsDiv.appendChild(updateButton); content.appendChild(buttonsDiv); configContainer.appendChild(content); // Add our config section to the page prefsContainer.appendChild(configContainer); }; // Initialize the script const init = () => { log('Initializing script...'); // Check if we're on the preferences page if (window.location.href.includes('preferences.php')) { createConfigUI(); return; } // We're on the item page, add the tracker button addTrackerButton(); log('Tracker button added'); // Add event listeners to the inventory tabs to ensure we can detect when tabs are changed const plushiesTab = document.querySelector('a[data-category="plushies"]'); const flowersTab = document.querySelector('a[data-category="flowers"]'); if (plushiesTab) { plushiesTab.addEventListener('click', () => { log('Plushies tab clicked'); setTimeout(scanPlushies, 500); }); } if (flowersTab) { flowersTab.addEventListener('click', () => { log('Flowers tab clicked'); setTimeout(scanFlowers, 500); }); } // Initial scan of inventory setTimeout(() => { log('Performing initial inventory scan...'); scanInventory(); }, 1000); }; // Run the script when the page is fully loaded window.addEventListener('load', init); // Also run when DOM content is loaded (as a backup) if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { log('Document already loaded, initializing immediately'); init(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址