您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Displays the progress value from the mining bar, while collecting data in the background and graphing it.
// ==UserScript== // @name Farm RPG Mining Progress Display // @namespace http://tampermonkey.net/ // @version 4.4 // @description Displays the progress value from the mining bar, while collecting data in the background and graphing it. // @author ClientCoin // @match http://farmrpg.com/* // @match https://farmrpg.com/* // @grant GM_getValue // @grant GM_setValue // @license MIT // @icon https://www.google.com/s2/favicons?sz=64&domain=farmrpg.com // ==/UserScript== (function() { 'use strict'; console.log('Mining Progress Display Initiated'); // --- CONFIGURATION --- const isDebugMode = false; // Set this to true to see debug logs const debugLog = (...args) => { if (isDebugMode) { console.log('Tampermonkey script:', ...args); } }; // The number of most recent data points to plot on the graph. const pointsToPlot = 25; // A scaling factor for font sizes on the graph (e.g., 1.0 = normal size, 0.8 = 20% smaller) const fontScaleFactor = 1.2; // --- END CONFIGURATION --- // A unique key for storing data in GM_storage const STORAGE_KEY = 'farmrpg_mining_data'; // =========================================== // SECTION 1: DATA COLLECTION LOGIC // - This section ONLY handles data storage. // - It does NOT modify the UI. // =========================================== let dataRetryCount = 0; const MAX_DATA_RETRIES = 5; // Function to get the current mining location name from the DOM function getMiningLocation() { debugLog('2. DATA: Executing getMiningLocation function...'); const allCenterSlidingDivs = document.querySelectorAll('.center.sliding'); debugLog(`2. DATA: - Found ${allCenterSlidingDivs.length} elements with class ".center.sliding"`); for (const div of allCenterSlidingDivs) { // Find the 'info' icon element inside the current div. const infoIcon = div.querySelector('a i.f7-icons'); // This is the most reliable way to identify the mining location header. if (infoIcon) { const fullText = div.textContent; // The location name is the part of the string before the 'info' text. const locationText = fullText.substring(0, fullText.indexOf('info')).trim(); debugLog(`2. DATA: - Found a match! Location element has 'info' icon. Location: ${locationText}`); return locationText; } } debugLog('2. DATA: - No matching element found. Returning null.'); return null; } function getCurrentFloor() { const floorLabel = document.querySelector('.col-30 strong'); const floorMatch = floorLabel.textContent.match(/(\d{1,3}(?:,\d{3})*)/); const currentFloor = floorMatch ? parseInt(floorMatch[1].replace(/,/g, ''), 10) : null; debugLog("Current Floor is: " + currentFloor); return currentFloor; } // Function to reset all stored data for the current mining location async function resetMiningData(locationName) { if (!locationName) { console.warn('Tampermonkey script: Cannot reset data, location name is not defined.'); return; } // Confirmation prompt to prevent accidental data loss // Using a simple custom modal for better UX than alert/confirm const userConfirmed = await new Promise(resolve => { const modal = document.createElement('div'); modal.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border: 1px solid #ccc; border-radius: 8px; z-index: 10000; box-shadow: 0 4px 8px rgba(0,0,0,0.2); `; modal.innerHTML = ` <p>Are you sure you want to delete all mining data for ${locationName}?</p> <button id="confirm-yes">Yes</button> <button id="confirm-no">No</button> `; document.body.appendChild(modal); document.getElementById('confirm-yes').onclick = () => { modal.remove(); resolve(true); }; document.getElementById('confirm-no').onclick = () => { modal.remove(); resolve(false); }; }); if (userConfirmed) { try { let miningData = await GM_getValue(STORAGE_KEY, {}); delete miningData[locationName]; await GM_setValue(STORAGE_KEY, miningData); console.log(`Tampermonkey script: Successfully deleted all mining data for ${locationName}.`); // Reload the page to refresh the display window.location.reload(); } catch (error) { console.error('Tampermonkey script: Error resetting data:', error); } } } async function collectAndStoreMiningData() { const progressBar = document.getElementById('mpb'); const floorElement = document.querySelector('.col-30 strong span'); const locationName = getMiningLocation(); debugLog('Starting collectAndStoreMiningData...'); debugLog('Location Name:', locationName); debugLog('Progress Bar Found:', !!progressBar); debugLog('Floor Element Found:', !!floorElement); if (!progressBar || !floorElement || !locationName) { if (dataRetryCount < MAX_DATA_RETRIES) { dataRetryCount++; setTimeout(collectAndStoreMiningData, 50); } else { dataRetryCount = 0; } return; } dataRetryCount = 0; const currentFloor = getCurrentFloor(); const currentProgress = parseFloat(progressBar.getAttribute('data-progress')); try { let miningData = await GM_getValue(STORAGE_KEY, {}); debugLog('Data retrieved from storage (before update):', miningData); // Store the data in the new nested format: location > floor > progress miningData[locationName] = miningData[locationName] || {}; miningData[locationName][currentFloor] = { progress: currentProgress }; await GM_setValue(STORAGE_KEY, miningData); debugLog('Data saved to storage (after update):', miningData); } catch (error) { console.error('Tampermonkey script: Error during data collection:', error); } } // =========================================== // SECTION 2: DISPLAY LOGIC (v1.1) // =========================================== function getRecentProgressPerFloor(locationData) { debugLog('1. DISPLAY: Calculating progress rate...'); const ALPHA = 0.2; const MAD_THRESHOLD = 2.5; const MAD_MINIMUM_PERCENTAGE_OF_MEDIAN = 0.1; const allFloors = Object.keys(locationData).map(Number).sort((a, b) => a - b); const recentFloors = allFloors.slice(Math.max(0, allFloors.length - 100)); if (recentFloors.length < 2) { debugLog('1. DISPLAY: Not enough data points to calculate a rate. (Need at least 2 floors)'); return null; } let progressRates = []; for (let i = 1; i < recentFloors.length; i++) { const previousFloor = recentFloors[i - 1]; const floorsGained = recentFloors[i] - previousFloor; if (floorsGained > 0) { const progressChange = locationData[recentFloors[i]].progress - locationData[previousFloor].progress; if (progressChange > 0) { progressRates.push(progressChange / floorsGained); } } } debugLog('1. DISPLAY: Original progress rates:', progressRates.map(rate => rate.toFixed(4))); if (progressRates.length < 3) { debugLog('1. DISPLAY: Not enough data points for statistical filtering. (Need at least 3)'); return null; } // --- Outlier detection using MAD --- const sortedRates = [...progressRates].sort((a, b) => a - b); const median = sortedRates.length % 2 === 0 ? (sortedRates[sortedRates.length / 2 - 1] + sortedRates[sortedRates.length / 2]) / 2 : sortedRates[Math.floor(sortedRates.length / 2)]; const deviations = sortedRates.map(rate => Math.abs(rate - median)); deviations.sort((a, b) => a - b); let mad = deviations.length % 2 === 0 ? (deviations[deviations.length / 2 - 1] + deviations[deviations.length / 2]) / 2 : deviations[Math.floor(deviations.length / 2)]; if (mad < 0.01) {mad = median * MAD_MINIMUM_PERCENTAGE_OF_MEDIAN}; const filteredRates = progressRates.filter(rate => Math.abs(rate - median) <= MAD_THRESHOLD * mad); const rejectedRates = progressRates.filter(rate => !filteredRates.includes(rate)); debugLog(`1. DISPLAY: Median: ${median.toFixed(4)}, MAD: ${mad.toFixed(4)}`); debugLog(`1. DISPLAY: Filtering data points within ${MAD_THRESHOLD}x MAD from median.`); debugLog('1. DISPLAY: Rejected rates (outliers):', rejectedRates.map(rate => rate.toFixed(4))); if (filteredRates.length === 0) { debugLog('1. DISPLAY: No valid data points remain after filtering.'); return null; } let weightedAverage = filteredRates[0]; for (let i = 1; i < filteredRates.length; i++) { weightedAverage = (ALPHA * filteredRates[i]) + ((1 - ALPHA) * weightedAverage); } debugLog(`1. DISPLAY: Final calculated weighted average: ${weightedAverage.toFixed(4)}`); return weightedAverage; } async function updateMiningDisplay() { debugLog('Starting updateMiningDisplay...'); const progressBar = document.getElementById('mpb'); const floorElement = document.querySelector('.col-30 strong span'); const locationName = getMiningLocation(); debugLog('Location Name:', locationName); debugLog('Progress Bar Found:', !!progressBar); debugLog('Floor Element Found:', !!floorElement); if (!progressBar || !floorElement || !locationName) { return; } const floorLabel = document.querySelector('.col-30 strong'); const currentFloor = getCurrentFloor(); if (isNaN(currentFloor)) { debugLog('1. DISPLAY: Floor number is not a valid number. Exiting.'); return; } if (floorLabel) { const textNode = Array.from(floorLabel.childNodes).find(node => node.nodeType === Node.TEXT_NODE && node.textContent.trim() === 'Floor'); if (textNode) { textNode.remove(); } const brElement = floorLabel.querySelector('br'); if (brElement) { brElement.remove(); } if (!floorLabel.querySelector('#floor-label')) { const labelSpan = document.createElement('span'); labelSpan.id = 'floor-label'; labelSpan.textContent = 'Floor: '; labelSpan.style.cssText = `font-size: 17px; font-weight: bold;`; floorLabel.prepend(labelSpan); const floorNumberSpan = floorLabel.querySelector('span:not(#floor-label)'); if(floorNumberSpan) floorNumberSpan.style.fontSize = '17px'; } } const currentProgress = parseFloat(progressBar.getAttribute('data-progress')); let miningData = await GM_getValue(STORAGE_KEY, {}); debugLog('Data retrieved for display:', miningData); const locationData = miningData[locationName] || {}; let floorsToGoText = 'Data gathering...'; let targetFloorText = 'Complete 4 Floors to Start'; let lastKnownProgress = 0; const allFloors = Object.keys(locationData).map(Number).sort((a, b) => b - a); if (allFloors.length > 0) { const lastFloorNumber = allFloors[0]; lastKnownProgress = locationData[lastFloorNumber]?.progress || 0; } debugLog('Stored location data:', miningData); if (currentProgress < lastKnownProgress) { miningData[locationName] = {}; await GM_setValue(STORAGE_KEY, miningData); } const progressRate = getRecentProgressPerFloor(locationData, currentFloor); if (progressRate && progressRate > 0) { const progressRemaining = 100 - currentProgress; const estimatedFloorsToGo = progressRemaining / progressRate; floorsToGoText = `Est. Floors: ${Math.round(estimatedFloorsToGo)}`; targetFloorText = `Target Floor: ${Math.round(currentFloor + estimatedFloorsToGo)}`; } // Function to create or update the display elements const createOrUpdateDisplay = (progressValue) => { var floorContainer = document.querySelector('.col-30'); floorContainer.innerHTML = floorContainer.innerHTML.replace(/<br>\s*<br>/g, '<br>'); if (floorContainer) { let progressDisplay = document.getElementById('farmrpg-progress-display'); let estimateDisplay = document.getElementById('farmrpg-estimate-display'); let targetFloorDisplay = document.getElementById('farmrpg-target-floor-display'); let resetButton = document.getElementById('farmrpg-reset-button'); // --- Create/Update Progress, Estimate, and Target Displays --- if (!progressDisplay) { progressDisplay = document.createElement('div'); progressDisplay.id = 'farmrpg-progress-display'; floorContainer.insertBefore(progressDisplay, floorContainer.querySelector('strong')); progressDisplay.style.cssText = `font-weight: bold !important; color: lightgreen !important; font-size: 14px !important; margin-bottom: 5px !important; text-shadow: 0 0 5px rgba(144, 238, 144, 0.5) !important;`; } const formattedProgress = parseFloat(progressValue).toFixed(2); progressDisplay.textContent = `Progress: ${formattedProgress}%`; if (!estimateDisplay) { estimateDisplay = document.createElement('div'); estimateDisplay.id = 'farmrpg-estimate-display'; floorContainer.insertBefore(estimateDisplay, floorContainer.querySelector('strong').nextSibling); estimateDisplay.style.cssText = `font-weight: bold !important; color: #add8e6 !important; font-size: 12px !important; margin-top: 5px !important;`; } estimateDisplay.textContent = floorsToGoText; if (!targetFloorDisplay) { targetFloorDisplay = document.createElement('div'); targetFloorDisplay.id = 'farmrpg-target-floor-display'; floorContainer.insertBefore(targetFloorDisplay, estimateDisplay.nextSibling); targetFloorDisplay.style.cssText = `font-weight: bold !important; color: #ffcc00 !important; font-size: 12px !important; margin-top: 5px !important;`; } targetFloorDisplay.textContent = targetFloorText; // --- Create/Update Reset Button --- if (!resetButton) { resetButton = document.createElement('a'); resetButton.id = 'farmrpg-reset-button'; resetButton.className = 'button btnpurple'; resetButton.style.cssText = `font-size: 11px; height: 20px; line-height: 18px; width: 100px; margin: 10px auto 0 auto; display: block;`; resetButton.textContent = 'Reset Data'; resetButton.onclick = () => resetMiningData(locationName); floorContainer.insertBefore(resetButton, targetFloorDisplay.nextSibling);//.replace(/<br><br>/g, '<br>'); } // --- Call the graph function --- createAndPlaceGraph(locationData); } }; createOrUpdateDisplay(currentProgress); } // =========================================== // SECTION 3: GRAPH LOGIC (UPDATED FOR CANVAS) // =========================================== let retryCount = 0; const MAX_RETRIES = 10; /** * Creates and places the graph in the correct position with a retry mechanism. * @param {object} locationData - The data object for the current mining location. */ function createAndPlaceGraph(locationData) { debugLog('Starting createAndPlaceGraph...'); // Use a more specific selector to find the correct row const parentRow = document.querySelector('.page[data-page="mining"] .row'); if (!parentRow) { console.error('Tampermonkey script: Could not find the correct parent .row element. Retrying...'); if (retryCount < MAX_RETRIES) { retryCount++; setTimeout(() => createAndPlaceGraph(locationData), 200); } return; } const leftCol = parentRow.querySelector('.col-30:first-of-type'); const centerCol = parentRow.querySelector('.col-40'); const rightCol = parentRow.querySelector('.col-30:last-of-type'); if (!leftCol || !centerCol || !rightCol) { console.error('Tampermonkey script: Could not find one of the required columns for re-positioning. Retrying...'); if (retryCount < MAX_RETRIES) { retryCount++; setTimeout(() => createAndPlaceGraph(locationData), 200); } return; } // Reset retry count on success retryCount = 0; // --- Step 1: Move content from rightCol to leftCol --- var rightColContent = rightCol.innerHTML.replace('<span style=\u0022font-size: 12px\u0022>Items Left</span>','').replace('<span style=\u0022font-size: 12px\u0022>Attempts Left</span>','').replace('<span style=\u0022font-size: 18px\u0022 id=\u0022attempts','Attempts Left: <span style=\u0022font-size: 12px\u0022 id=\u0022attempts').replace('<span style=\u0022font-size: 18px\u0022 id=\u0022items','Items Left: <span style=\u0022font-size: 12px\u0022 id=\u0022items').replace(/<br><br>/g, '<br>').replace('</strong><br>','</strong>'); //<span style="font-size: 12px"></span> //<span style="font-size: 18px" id="items">3</span> //<span style="font-size: 18px" id="attempts">22</span> const newContentDiv = document.createElement('div'); newContentDiv.innerHTML = rightColContent; leftCol.appendChild(newContentDiv); debugLog('Moved content from right column to left column.'); // --- Step 2: Remove the original rightCol --- parentRow.removeChild(rightCol); debugLog('Removed the original right column.'); // --- Step 3: Resize the centerCol --- centerCol.className = 'col-20'; debugLog('Resized center column to col-20.'); // --- Step 4: Create a new col-50 for the graph --- let graphCol = document.getElementById('farmrpg-graph-col'); if (!graphCol) { graphCol = document.createElement('div'); graphCol.id = 'farmrpg-graph-col'; graphCol.className = 'col-50'; parentRow.insertBefore(graphCol, centerCol.nextSibling); debugLog('Created a new col-50 for the graph.'); } // --- Step 5: Place the graph inside the new col-50 --- let graphContainer = document.getElementById('farmrpg-graph-container'); if (!graphContainer) { graphContainer = document.createElement('div'); graphContainer.id = 'farmrpg-graph-container'; graphContainer.style.cssText = ` margin-top: 20px; text-align: center; `; graphCol.appendChild(graphContainer); debugLog('Placed the graph container in the new col-50.'); } let graphCanvas = document.getElementById('farmrpg-progress-graph'); let messageDiv = document.getElementById('farmrpg-graph-message'); // Prepare data for the graph and filter out non-numeric keys let floors = Object.keys(locationData) .filter(key => !isNaN(Number(key))) // Filter out non-numeric keys .map(Number) .sort((a, b) => a - b); debugLog('Floors array from data (filtered):', floors); debugLog('Number of floors:', floors.length); // --- Filter to only the last X points --- floors = floors.slice(Math.max(0, floors.length - pointsToPlot)); debugLog(`Filtered to last ${pointsToPlot} floors:`, floors); // If there are less than two data points, show a message instead of a graph if (floors.length < 2) { if (messageDiv) messageDiv.remove(); if (graphCanvas) graphCanvas.remove(); messageDiv = document.createElement('div'); messageDiv.id = 'farmrpg-graph-message'; messageDiv.textContent = 'Mine more to see your progress graph!'; graphContainer.appendChild(messageDiv); return; } // If there's enough data, ensure the graph canvas exists if (!graphCanvas) { graphCanvas = document.createElement('canvas'); graphCanvas.id = 'farmrpg-progress-graph'; graphCanvas.width = graphCol.clientWidth; graphCanvas.height = 200; graphCanvas.style.cssText = ` background-color: rgba(0, 0, 0, 0.4); border-radius: 8px; `; graphContainer.appendChild(graphCanvas); } if (messageDiv) messageDiv.remove(); // Get the canvas context const ctx = graphCanvas.getContext('2d'); // Clear the canvas for redrawing ctx.clearRect(0, 0, graphCanvas.width, graphCanvas.height); // Define drawing area and scaling const padding = 20; const xOffset = 30; // Space for Y-axis labels const graphWidth = graphCanvas.width - padding * 2 - xOffset; const graphHeight = graphCanvas.height - padding * 2; // Find min/max values for scaling const minFloor = floors[0]; const maxFloor = floors[floors.length - 1]; // Calculate dynamic min and max progress values const lastProgressValues = floors.map(floor => locationData[floor]?.progress).filter(p => p !== undefined); const minRawProgress = Math.min(...lastProgressValues); const maxRawProgress = Math.max(...lastProgressValues); // Add a 5% buffer to the min and max values for better visualization const progressRange = maxRawProgress - minRawProgress; const paddedMinProgress = Math.max(0, minRawProgress - progressRange * 0.05); const paddedMaxProgress = Math.min(100, maxRawProgress + progressRange * 0.05); debugLog(`minFloor: ${minFloor}, maxFloor: ${maxFloor}, floorRange: ${maxFloor - minFloor}`); debugLog(`minProgress: ${paddedMinProgress.toFixed(2)}, maxProgress: ${paddedMaxProgress.toFixed(2)}`); // Draw the graph title ctx.fillStyle = '#fff'; ctx.font = `${16 * fontScaleFactor}px Arial`; ctx.textAlign = 'center'; ctx.fillText('Mining Progress Over Time', graphCanvas.width / 2, padding / 2 + 10); // Draw axes and labels ctx.strokeStyle = '#ddd'; ctx.fillStyle = '#ddd'; ctx.lineWidth = 1; // Y-axis ctx.beginPath(); ctx.moveTo(padding + xOffset, padding); ctx.lineTo(padding + xOffset, graphHeight + padding); ctx.stroke(); // X-axis ctx.beginPath(); ctx.moveTo(padding + xOffset, graphHeight + padding); ctx.lineTo(graphWidth + padding + xOffset, graphHeight + padding); ctx.stroke(); // Label Y-axis ctx.textAlign = 'right'; ctx.font = `${12 * fontScaleFactor}px Arial`; ctx.fillText(paddedMaxProgress.toFixed(1) + '%', padding + xOffset - 5, padding + 5); ctx.fillText(paddedMinProgress.toFixed(1) + '%', padding + xOffset - 5, graphHeight + padding - 5); // Label X-axis (with min/max floor numbers) ctx.font = `${12 * fontScaleFactor}px Arial`; ctx.textAlign = 'left'; ctx.fillText(minFloor, padding + xOffset, graphHeight + padding + 15); ctx.textAlign = 'right'; ctx.fillText(maxFloor, graphWidth + padding + xOffset, graphHeight + padding + 15); // --- Plot the line --- ctx.strokeStyle = '#ffcc00'; ctx.lineWidth = 2; ctx.beginPath(); floors.forEach((floor, index) => { const dataPoint = locationData[floor]; if (!dataPoint) return; const progress = dataPoint.progress; let x; const floorRange = maxFloor - minFloor; if (floorRange <= 1) { x = padding + xOffset + (index / (floors.length - 1)) * graphWidth; } else { x = padding + xOffset + ((floor - minFloor) / floorRange) * graphWidth; } const y = graphHeight + padding - ((progress - paddedMinProgress) / (paddedMaxProgress - paddedMinProgress)) * graphHeight; if (index === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } }); ctx.stroke(); // --- Plot the points on top of the line --- floors.forEach((floor, index) => { const dataPoint = locationData[floor]; if (!dataPoint) return; const progress = dataPoint.progress; let x; const floorRange = maxFloor - minFloor; if (floorRange <= 1) { x = padding + xOffset + (index / (floors.length - 1)) * graphWidth; } else { x = padding + xOffset + ((floor - minFloor) / floorRange) * graphWidth; } const y = graphHeight + padding - ((progress - paddedMinProgress) / (paddedMaxProgress - paddedMinProgress)) * graphHeight; ctx.fillStyle = '#ffcc00'; ctx.beginPath(); ctx.arc(x, y, 4, 0, Math.PI * 2); ctx.fill(); }); } // =========================================== // SECTION 4: ACTIVATION // - This section activates both functions. // =========================================== updateMiningDisplay(); collectAndStoreMiningData(); const targetNode = document.querySelector("#fireworks"); if (targetNode) { const navObserver = new MutationObserver((mutationsList) => { if (mutationsList.some(m => m.attributeName === 'data-page')) { debugLog('3. ACTIVATION: Navigation detected via data-page attribute change.'); updateMiningDisplay(); collectAndStoreMiningData(); } }); navObserver.observe(targetNode, { attributes: true }); debugLog('3. ACTIVATION: Navigation observer set up.'); } })();