DeepCo Statistics

Track blocks mined and RC yield rate

// ==UserScript==
// @name         DeepCo Statistics
// @namespace    https://deepco.app/
// @version      2025-07-17
// @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==

(async function() {
  'use strict';

  // Load existing logs or initialize with header row
  let db = await GM.getValue('nudgeLogs', [['Timestamp', 'TileCount', 'RC', 'Level', 'DC', 'Players', 'DCIncome', 'Processing Rating']]);

  new MutationObserver((mutation, observer) => {
    const deptScaling = document.querySelector('.department-scaling');
    if (deptScaling) {
      observer.disconnect();
      console.log("[DeepCo Stats] Started");
      waitForTargetAndObserve()
    }
  }).observe(document.body, { childList: true, subtree: true });

  function waitForTargetAndObserve() {
    const frame = document.getElementById('flash-messages');
    const footer = document.querySelector('[class^="grid-footer"]');
    createPanel(footer);

    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.startsWith('You got paid!')) {
            logStats(text);
          }
        }
      }
    }).observe(frame, { childList: true, subtree: false });

    console.log('[DeepCo Stats] Observer attached to parent frame.');
  }

  function createPanel(parentElement) {
    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);

    parentElement.appendChild(btnContainer);
  }

  async function logStats(flashMessage) {
    const timestamp = getTimestampForSheets();
    const tileCount = getTileCount();
    const rc = getRCCount();
    const level = getLevel();
    const dc = getDCCount();
    const players = countPlayersInLevel();
    const dcIncome = getDCIncome(flashMessage);
    const rating = getProcessingRating();

    db.push([timestamp, tileCount, rc, level, dc, players, dcIncome, rating]);

    await GM.setValue('nudgeLogs', db);

    // console.log(`[DeepCo Stats] ${timestamp}: ${tileCount}, ${rc}, ${level}, ${dc}, ${players}, ${dcIncome}`);
  }

  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 = `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?')) {
      db = [['Timestamp', 'TileCount', 'RC', 'Level', 'DC', 'Players', 'DCIncome', 'Processing Rating']];
      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}`;
  }

  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 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(/You got paid!\s*([\d,]+(?:\.\d+)?)/);
    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;
  }
})();