// ==UserScript==
// @name DeepCo Statistics
// @namespace https://deepco.app/
// @version 2025-07-22v3
// @description Track RC rate and chart it.
// @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
// @require https://code.highcharts.com/highcharts.js
// @require https://code.highcharts.com/modules/boost.js
// @require https://code.highcharts.com/modules/mouse-wheel-zoom.js
// ==/UserScript==
(async function() {
'use strict';
const SCHEMA = [['Timestamp', 'TileCount', 'RC', 'Level', 'DC', 'Players', 'DCIncome', 'Processing Rating']];
// Load existing logs or initialize with header row
let db = await GM.getValue('nudgeLogs', SCHEMA);
fixTimestamps(db);
let myChart = null;
let recursionTime = null;
new MutationObserver((mutation, observer) => {
const deptScaling = document.querySelector('.department-scaling');
if (deptScaling) {
observer.disconnect();
console.log("[DeepCo Stats] Started");
applyFontPatch();
waitForTargetAndObserve()
}
}).observe(document.body, { childList: true, subtree: true });
function waitForTargetAndObserve() {
const frame = document.getElementById('flash-messages');
createPanel();
const observer = new MutationObserver((mutationsList, observer) => {
for (const mutation of mutationsList) {
for (const node of mutation.addedNodes) {
if (node.nodeType !== Node.ELEMENT_NODE) continue;
const text = node.textContent.trim();
if (text.includes('[DC]')) {
logStats(text);
}
}
}
}).observe(frame, { childList: true, subtree: false });
}
function createPanel() {
const grid = document.getElementById('grid-panel');
const parentElement = document.createElement("div");
parentElement.className = 'grid-wrapper';
// add buttons
const btnContainer = document.createElement("div");
btnContainer.style.display = 'flex';
btnContainer.style.justifyContent = 'center';
btnContainer.style.gap = '10px'; // optional spacing between buttons
// Export button
const exportBtn = document.createElement("button");
exportBtn.textContent = "Export Player Stats";
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);
// 2. Create container for buttons
const chartContainer = document.createElement('div');
chartContainer.id = 'hc-container';
// chartContainer.style.height = '300px';
// chartContainer.style.backgroundColor = '#fff';
// chartContainer.style.border = '1px solid #ccc';
// chartContainer.style.boxShadow = '0 0 10px rgba(0,0,0,0.3)';
parentElement.appendChild(chartContainer);
parentElement.appendChild(btnContainer);
grid.appendChild(parentElement);
const playerSpan = document.querySelector('.small-link span[style*="color"]');
const playerColor = playerSpan.style.color;
// 3. Render Highcharts chart
myChart = Highcharts.chart('hc-container', {
chart: {
zooming: {
type: 'x',
mouseWheel: {
enabled: true,
type: 'x'
}
}
},
title: { text: 'RC Yield' },
subtitle: {
text: document.ontouchstart === undefined ?
'Click and drag in the plot area to zoom in' :
'Pinch the chart to zoom in'
},
xAxis: {
type: 'datetime'
},
yAxis: {
title: { text: 'RC/hr' }
},
series: [{
name: 'RC/hr',
data: calcData(),
color: playerColor
}],
tooltip: {
xDateFormat: '%Y-%m-%d %H:%M:%S.%L',
pointFormat: 'RC/hr: <b>{point.y:.2f}</b><br/>'
}
});
}
function calcData() {
const seriesData = [];
// start at 1 to skip header
for (let i = 1; i < db.length; i++) {
// first row of data or RC decrease -> set recursion time to now
if (i === 0 || db[i][2] < db[i-1][2]) {
recursionTime = db[i][0];
}
const timeElapsed = (db[i][0] - recursionTime) / (1000 * 60 * 60);
const rcPerMin = db[i][2] === 0 ? 0 : db[i][2] / timeElapsed;
seriesData.push([db[i][0], rcPerMin]);
}
return seriesData;
}
async function logStats(flashMessage) {
const timestamp = Date.now();
const tileCount = getTileCount();
const rc = getRCCount();
const level = getLevel();
const dc = getDCCount();
const players = countPlayersInLevel();
const dcIncome = getDCIncome(flashMessage);
const rating = getProcessingRating();
// push to database
db.push([timestamp, tileCount, rc, level, dc, players, dcIncome, rating]);
await GM.setValue('nudgeLogs', db);
const timeElapsed = (timestamp - recursionTime) / (1000 * 60 * 60);
const rcPerMin = rc === 0 ? 0 : rc / timeElapsed;
// update chart
myChart.series[0].addPoint([timestamp, rcPerMin], true, false);
scrollChartToEnd(myChart);
}
async function exportStats() {
const logs = await GM.getValue('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 = `dc_player_stats_${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?')) {
db = SCHEMA;
await GM.setValue('nudgeLogs', db);
alert('Tile logs have been cleared.');
}
}
function getTimestampForSheets() {
const d = new Date();
const pad = (n, z = 2) => String(n).padStart(z, '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());
const milliseconds = pad(d.getMilliseconds(), 3);
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
}
// convert to ISO 8601 format i.e. milliseconds since epoch
function fixTimestamps(db) {
for (let i = 1; i < db.length; i++) {
const ts = db[i][0];
if (typeof ts === 'string') {
db[i][0] = new Date(ts.replace(' ', 'T')).getTime();
}
}
}
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();
value = parseInt(value.replace(/[^\d]/g, ""), 10);
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 getDCCount() {
// Find the UPGRADES link with badge
const upgradesLink = Array.from(document.querySelectorAll('a')).find(a =>
a.textContent.includes('UPGRADES'));
// Parse current DC value from its text
const match = upgradesLink.textContent.match(/\[DC\]\s*([\d,.]+)/);
const currentDC = match ? parseFloat(match[1].replace(/,/g, '')) : 0;
return currentDC;
}
// includes the current player
function countPlayersInLevel() {
const deptScaling = document.querySelector('.department-scaling');
const deptOperators = deptScaling ? deptScaling.querySelectorAll('a').length : 0;
return deptOperators;
}
function getDCIncome(flashMessage) {
// parse flashMessage which has DC income amount
const match = flashMessage.match(/\b\d+(\.\d+)?\s*\[DC\]/i);
const amount = parseFloat(match[1].replace(/,/g, ''));
return amount;
}
function getProcessingRating() {
const rating = Array.from(document.querySelectorAll('.stat-item'))
.map(item => {
const label = item.querySelector('.stat-label');
const value = item.querySelector('.stat-value');
return label && label.textContent.trim() === 'Processing Rating:' && value
? parseFloat(value.textContent.trim())
: null;
})
.filter(Boolean)[0] || null;
return rating;
}
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;
}
function scrollChartToEnd(chart, bufferRatio = 0.01) {
const xAxis = chart.xAxis[0];
const extremes = xAxis.getExtremes();
const viewSize = extremes.max - extremes.min;
const dataMax = extremes.dataMax;
const isAtEnd = Math.abs(extremes.max - dataMax) < viewSize * 0.05; // ~5% tolerance
if (isAtEnd) {
const buffer = viewSize * bufferRatio;
const newMax = dataMax + buffer;
const newMin = newMax - viewSize;
xAxis.setExtremes(newMin, newMax);
}
}
// stylise highcharts font
function applyFontPatch() {
if (typeof Highcharts === 'undefined' || !document.body) {
requestAnimationFrame(applyFontPatch);
return;
}
const bodyStyles = getComputedStyle(document.body);
const bodyFont = bodyStyles.fontFamily;
const fontColor = bodyStyles.color;
const backgroundColor = bodyStyles.backgroundColor;
Highcharts.setOptions({
chart: {
backgroundColor: 'rgb(17, 17, 17)',
style: {
color: fontColor,
fontFamily: bodyFont
}
},
title: {
style: {
color: fontColor
}
},
subtitle: {
style: {
color: fontColor
}
},
xAxis: {
labels: {
style: {
color: fontColor
}
},
title: {
style: {
color: fontColor
}
}
},
yAxis: {
labels: {
style: {
color: fontColor
}
},
title: {
style: {
color: fontColor
}
}
},
legend: {
itemStyle: {
color: fontColor
}
},
tooltip: {
backgroundColor: backgroundColor,
style: {
color: fontColor
}
},
});
}
})();