Cybroria Loot Tracker (Enhanced)

Tracks loot and stat drops from right-hand log in Cybroria, now with CSV export, drag, reset, and persistent storage features.

目前为 2025-05-11 提交的版本。查看 最新版本

// ==UserScript==
// @name         Cybroria Loot Tracker (Enhanced)
// @namespace    http://tampermonkey.net/
// @version      3.0
// @description  Tracks loot and stat drops from right-hand log in Cybroria, now with CSV export, drag, reset, and persistent storage features.
// @author       Skydragon
// @match        https://cybroria.com/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const STORAGE_KEY = 'cybroria_loot_stats';
    const POS_KEY = 'cybroria_tracker_position';
    let timestamps = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
    let viewMode = 'hour'; // 'hour', 'day', or 'session'

    const trackedTypes = [
        "Strength", "Agility", "Dexterity", "Vitality",
        "Energy Tap", "System Breach", "Chemsynthesis", "Cyber Harvest",
        "Credits", "Power Cells"
    ];

    const trackerBox = document.createElement('div');
    trackerBox.style.position = 'fixed';
    trackerBox.style.top = localStorage.getItem(POS_KEY + '_top') || '10px';
    trackerBox.style.left = localStorage.getItem(POS_KEY + '_left') || '10px';
    trackerBox.style.background = '#000';
    trackerBox.style.color = '#0f0';
    trackerBox.style.padding = '10px';
    trackerBox.style.fontSize = '13px';
    trackerBox.style.fontFamily = 'monospace';
    trackerBox.style.zIndex = 9999;
    trackerBox.style.cursor = 'move';
    trackerBox.style.minWidth = '200px';
    trackerBox.style.border = '1px solid #0f0';
    trackerBox.innerHTML = '<strong>Cybroria Loot Tracker</strong><br>';
    document.body.appendChild(trackerBox);

    makeDraggable(trackerBox);
    renderControls();

    function makeDraggable(el) {
        let offsetX, offsetY, dragging = false;

        el.addEventListener('mousedown', (e) => {
            if (e.target.tagName === 'BUTTON' || e.target.tagName === 'SELECT') return;
            dragging = true;
            offsetX = e.clientX - el.offsetLeft;
            offsetY = e.clientY - el.offsetTop;
        });

        document.addEventListener('mousemove', (e) => {
            if (!dragging) return;
            el.style.left = `${e.clientX - offsetX}px`;
            el.style.top = `${e.clientY - offsetY}px`;
        });

        document.addEventListener('mouseup', () => {
            if (dragging) {
                localStorage.setItem(POS_KEY + '_top', el.style.top);
                localStorage.setItem(POS_KEY + '_left', el.style.left);
            }
            dragging = false;
        });
    }

    function renderControls() {
        const controls = document.createElement('div');
        controls.style.marginTop = '8px';

        const resetBtn = document.createElement('button');
        resetBtn.textContent = 'Reset';
        resetBtn.onclick = () => {
            if (confirm("Reset all loot stats?")) {
                timestamps = [];
                localStorage.removeItem(STORAGE_KEY);
                updateBox();
            }
        };

        const exportBtn = document.createElement('button');
        exportBtn.textContent = 'Export CSV';
        exportBtn.style.marginLeft = '6px';
        exportBtn.onclick = exportCSV;

        const modeSelect = document.createElement('select');
        modeSelect.style.marginLeft = '6px';
        ['hour', 'day', 'session'].forEach(mode => {
            const opt = document.createElement('option');
            opt.value = mode;
            opt.textContent = `Per ${mode}`;
            modeSelect.appendChild(opt);
        });
        modeSelect.value = viewMode;
        modeSelect.onchange = () => {
            viewMode = modeSelect.value;
            updateBox();
        };

        controls.appendChild(resetBtn);
        controls.appendChild(exportBtn);
        controls.appendChild(modeSelect);
        trackerBox.appendChild(controls);
    }

    function updateBox() {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(timestamps));

        const now = Date.now();
        const hourAgo = now - 3600000;
        const dayAgo = now - 86400000;

        const stats = {};
        timestamps.forEach(entry => {
            const show = viewMode === 'session' ||
                         (viewMode === 'hour' && entry.time >= hourAgo) ||
                         (viewMode === 'day' && entry.time >= dayAgo);
            if (!show) return;

            if (!stats[entry.item]) stats[entry.item] = 0;
            stats[entry.item] += entry.amount;
        });

        let html = '<strong>Cybroria Loot Tracker</strong><br><br>';
        html += `<u>Per ${viewMode.charAt(0).toUpperCase() + viewMode.slice(1)}:</u><br>`;
        for (const stat of trackedTypes) {
            if (stats[stat]) {
                html += `${stat}: ${stats[stat]}<br>`;
            }
        }

        trackerBox.innerHTML = html;
        renderControls();
    }

    function exportCSV() {
        let csv = 'Item,Amount\n';
        const totals = {};

        timestamps.forEach(t => {
            if (!totals[t.item]) totals[t.item] = 0;
            totals[t.item] += t.amount;
        });

        for (const item in totals) {
            csv += `${item},${totals[item]}\n`;
        }

        const blob = new Blob([csv], { type: 'text/csv' });
        const url = URL.createObjectURL(blob);
        const link = document.createElement('a');
        link.href = url;
        link.download = 'cybroria_loot.csv';
        link.click();
        URL.revokeObjectURL(url);
    }

    function parseLootText(text) {
        text = text.replace(/\u00A0/g, ' ')
                   .replace(/\s+/g, ' ')
                   .replace(/\([\d,]+ for your syndicate\)/g, '')
                   .trim();

        const statValueMatch = text.match(/You have found ([\d,]+) ([A-Za-z ]+?) Stat Value/i);
        if (statValueMatch) {
            const amount = parseInt(statValueMatch[1].replace(/,/g, ''), 10);
            const statName = statValueMatch[2].trim();
            if (trackedTypes.includes(statName)) {
                timestamps.push({ time: Date.now(), item: statName, amount });
                updateBox();
            }
            return;
        }

        const lootMatch = text.match(/You have found ([\d,]+) ([A-Za-z ]+)/);
        if (lootMatch) {
            const qty = parseInt(lootMatch[1].replace(/,/g, ''), 10);
            const item = lootMatch[2].trim();
            if (trackedTypes.includes(item)) {
                timestamps.push({ time: Date.now(), item: item, amount: qty });
                updateBox();
            }
        }
    }

    function observeLootLog() {
        const seenLines = new Set();
        setInterval(() => {
            const logSpans = document.querySelectorAll('app-loot-log span.ng-star-inserted');
            logSpans.forEach(span => {
                const rawText = span.textContent.trim();
                if (rawText.includes("You have found") && !seenLines.has(rawText)) {
                    seenLines.add(rawText);
                    parseLootText(rawText);
                }
            });
        }, 1000);
    }

    window.addEventListener('load', () => {
        setTimeout(observeLootLog, 3000);
        updateBox();
    });
})();