Track blocks mined and RC yield rate
当前为
// ==UserScript==
// @name DeepCo Statistics
// @namespace https://deepco.app/
// @version 2025-07-09v3
// @description Track blocks mined and RC yield rate
// @author Corns
// @match https://deepco.app/dig
// @icon https://www.google.com/s2/favicons?sz=64&domain=deepco.app
// @license MIT
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function() {
'use strict';
// Universal GM API wrapper for cross-compatibility
const GM = {
get: async (key, def) =>
typeof GM_getValue === 'function'
? Promise.resolve(GM_getValue(key, def))
: GM.getValue(key, def),
set: async (key, val) =>
typeof GM_setValue === 'function'
? Promise.resolve(GM_setValue(key, val))
: GM.setValue(key, val)
};
if (document.readyState === 'loading') {
window.addEventListener('DOMContentLoaded', waitForTargetAndObserve);
} else {
waitForTargetAndObserve();
}
function waitForTargetAndObserve() {
const frame = document.getElementById('tiles-defeated-badge');
if (!frame) {
// Retry in 500ms until the frame is present
setTimeout(waitForTargetAndObserve, 500);
return;
}
console.log('[DeepCo Stats] Watching frame:', frame);
const bonusPanel = document.querySelector('[class^="grid-footer"]');
const btnContainer = document.createElement("div");
// Export button
const exportBtn = document.createElement("button");
exportBtn.textContent = "Export Player Stats";
exportBtn.style.marginRight = "5px";
exportBtn.addEventListener("click", exportStats);
btnContainer.appendChild(exportBtn);
// Reset button
const resetBtn = document.createElement("button");
resetBtn.textContent = "Reset Stats";
resetBtn.addEventListener("click", resetStats);
btnContainer.appendChild(resetBtn);
bonusPanel.appendChild(btnContainer);
let lastValue = null;
const observer = new MutationObserver(() => {
// Always find the latest <strong>
let target =
frame.querySelector('strong.nudge-animation') ||
frame.querySelector('span.tile-progression strong[style*="inline-block"]');
if (!target) {
console.log('[DeepCo Stats] Target element not found in frame.');
return;
}
let value = target.textContent.trim().replace(/,/g, '').replace(/\/$/, '');
if (value !== lastValue) {
lastValue = value;
logStats(); // Fire your logger when the value is different
}
});
observer.observe(frame, {
childList: true, // detect node adds/removes
subtree: true, // include all descendants
characterData: true // detect text changes
});
console.log('[DeepCo Stats] Observer attached to parent frame.');
}
async function logStats() {
const tileCount = getTileCount();
const rc = getRCCount();
const level = getLevel();
const timestamp = getTimestampForSheets();
// Load existing logs or initialize with header row
let logs = await GM.get('nudgeLogs', [['Timestamp', 'TileCount', 'RC', 'Level']]);
// Check if last logged tileCount is different from current tileCount
// shouldn't ever happen because observer checks against this already
const lastValue = logs.length > 1 ? logs[logs.length - 1][1] : null;
if (lastValue === tileCount) {
// Same as last tileCount, do not log
return;
}
logs.push([timestamp, tileCount, rc, level]);
await GM.set('nudgeLogs', logs);
// console.log(`[DeepCo Stats] ${timestamp}: ${tileCount}, ${rc}, ${level}`);
}
async function exportStats() {
const logs = await GM.get('nudgeLogs', []);
if (logs.length === 0) {
console.log('[DeepCo Stats] No logs to save.');
return;
}
// Wrap values with commas in quotes
const csvContent = logs.map(row =>
row.map(value =>
/,/.test(value) ? `"${value}"` : value
).join(',')
).join('\n');
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `nudge_log_${new Date().toISOString().replace(/[:.]/g, "_")}.csv`;
link.click();
URL.revokeObjectURL(url);
console.log('[CSV Export] Downloaded CSV with', logs.length, 'rows.');
}
async function resetStats() {
if (confirm('Are you sure you want to clear player stats?')) {
await GM.set('nudgeLogs', [['Timestamp', 'TileCount', 'RC', 'Level']]);
alert('Tile logs have been cleared.');
}
}
function getTimestampForSheets() {
const d = new Date();
const pad = (n) => n.toString().padStart(2, '0');
const year = d.getFullYear();
const month = pad(d.getMonth() + 1);
const day = pad(d.getDate());
const hours = pad(d.getHours());
const minutes = pad(d.getMinutes());
const seconds = pad(d.getSeconds());
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
function getTileCount() {
const frame = document.getElementById('tiles-defeated-badge');
if (!frame) {
console.log('[DeepCo Stats] turbo-frame element not found.');
return;
}
// Try strong with class nudge-animation first
let target = frame.querySelector('strong.nudge-animation');
// If not found, try strong inside span.tile-progression with style containing 'inline-block'
if (!target) {
target = frame.querySelector('span.tile-progression strong[style*="inline-block"]');
}
if (!target) {
console.log('[DeepCo Stats] Target element not found inside turbo-frame.');
return;
}
let value = target.textContent.trim();
// Remove commas
value = value.replace(/,/g, '');
// Remove trailing slash
value = value.replace(/\/$/, '');
return value;
}
function getRCCount() {
// Find RC value
const recursionSpan = document.getElementById('recursion-header');
let rc = 0;
if (recursionSpan) {
const a = recursionSpan.querySelector('a');
if (a) {
// Extract RC value using regex, e.g. [+15 RC]
const rcMatch = a.textContent.match(/\[\+([\d.]+)\s*RC\]/);
if (rcMatch) {
rc = parseFloat(rcMatch[1]);
}
}
}
return rc;
}
function getLevel() {
// Find the department-stats element
const deptStats = document.querySelector('p.department-stats');
let dcValue = 0; // default if not found
if (deptStats) {
const text = deptStats.textContent.trim();
// Match DC followed by optional + and digits, e.g., DC4A or DC+4
const match = text.match(/DC\+?(\d+)/i);
if (match) {
dcValue = parseInt(match[1], 10);
}
}
return dcValue;
}
})();