DeepCo Statistics

Track blocks mined and RC yield rate

当前为 2025-07-09 提交的版本,查看 最新版本

// ==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;
  }
})();