Cybroria Loot Tracker (Soft Glow Edition)

Tracks loot, stats, Cyber Component gains, income value, and more with a soft-glow UI, draggable panel, CSV export, and persistent session data.

当前为 2025-05-11 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Cybroria Loot Tracker (Soft Glow Edition)
// @namespace    http://tampermonkey.net/
// @version      4.1
// @description  Tracks loot, stats, Cyber Component gains, income value, and more with a soft-glow UI, draggable panel, CSV export, and persistent session data.
// @author       Skydragon
// @match        https://cybroria.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

/*
📦 Cybroria Loot Tracker (v4.1 – Soft Glow Edition)

🆕 Features & Enhancements:
────────────────────────────
✨ Soft Glow UI:
   - Sleek, semi-transparent tracker panel with soft green glow and rounded corners

🧲 Draggable + Sticky Position:
   - Move the tracker anywhere on the screen; it remembers its position

🕒 Session Timer:
   - Tracks playtime since last reset

📊 View Mode Toggle:
   - Switch between Per Hour, Per Day, or Full Session stats

💰 Income Calculator:
   - Add your own credit values to loot drops and see total income

⚙️ Settings Panel:
   - Click ⚙️ to configure prices for loot items like Power Cells, Artifacts, etc.

🧠 Cyber Component Gain Tracking:
   - Auto-tracks your Cyber Component gains since last reset

📤 CSV Export:
   - One-click export of all tracked loot into a spreadsheet

🔁 Reset Button:
   - Clears all tracked loot + timer

────────────────────────────

📌 Works on: https://cybroria.com/
👥 Built by: Skydragon + requested enhancements
📄 License: MIT
*/

// ==/UserScript==

(function () {
    'use strict';

    const STORAGE_KEY = 'cybroria_loot_stats';
    const VALUE_KEY = 'cybroria_loot_values';
    const POS_KEY = 'cybroria_tracker_position';
    const RESET_TIME_KEY = 'cybroria_reset_time';
    const CYBER_BASE_KEY = 'cybroria_cyber_components_base';
    const CYBER_GAIN_KEY = 'cybroria_cyber_components_gain';

    let timestamps = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
    let lootValues = JSON.parse(localStorage.getItem(VALUE_KEY) || '{}');
    let viewMode = 'hour';

    if (!localStorage.getItem(RESET_TIME_KEY)) {
        localStorage.setItem(RESET_TIME_KEY, Date.now());
    }

    if (!localStorage.getItem(CYBER_GAIN_KEY)) {
        localStorage.setItem(CYBER_GAIN_KEY, '0');
    }

    const trackedTypes = [
        "Strength", "Agility", "Dexterity", "Vitality",
        "Energy Tap", "System Breach", "Chemsynthesis", "Cyber Harvest",
        "Credits", "Power Cells", "Logic Cores", "Artifacts",
        "Neuro Stims", "Cyber Implants", "Cyber Components"
    ];

    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 = 'rgba(0, 0, 0, 0.6)';
    trackerBox.style.color = '#cfc';
    trackerBox.style.padding = '12px';
    trackerBox.style.fontSize = '13px';
    trackerBox.style.fontFamily = 'monospace';
    trackerBox.style.zIndex = 9999;
    trackerBox.style.cursor = 'move';
    trackerBox.style.minWidth = '240px';
    trackerBox.style.borderRadius = '10px';
    trackerBox.style.boxShadow = '0 0 12px rgba(0, 255, 100, 0.4)';
    trackerBox.style.border = '1px solid rgba(0, 255, 100, 0.25)';
    trackerBox.style.textShadow = '0 0 4px rgba(0, 255, 100, 0.3)';
    document.body.appendChild(trackerBox);

    const timer = document.createElement('div');
    timer.id = 'cybroria-timer';
    timer.style.marginTop = '8px';
    timer.style.fontSize = '11px';
    timer.style.color = '#8f8';
    trackerBox.appendChild(timer);

    makeDraggable(trackerBox);
    renderControls();
    updateTimer();
    setInterval(updateTimer, 1000);

    function makeDraggable(el) {
        let offsetX, offsetY, dragging = false;
        el.addEventListener('mousedown', (e) => {
            if (e.target.tagName === 'BUTTON' || e.target.tagName === 'SELECT' || e.target.classList.contains('icon')) 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);
                localStorage.setItem(RESET_TIME_KEY, Date.now());
                localStorage.setItem(CYBER_GAIN_KEY, '0');
                localStorage.removeItem(CYBER_BASE_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();
        };

        const settingsBtn = document.createElement('span');
        settingsBtn.textContent = '⚙️';
        settingsBtn.title = 'Set values';
        settingsBtn.className = 'icon';
        settingsBtn.style.marginLeft = '8px';
        settingsBtn.style.cursor = 'pointer';
        settingsBtn.onclick = showSettingsPopup;

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

    function updateTimer() {
        const resetTime = parseInt(localStorage.getItem(RESET_TIME_KEY), 10);
        const seconds = Math.floor((Date.now() - resetTime) / 1000);
        const hrs = String(Math.floor(seconds / 3600)).padStart(2, '0');
        const mins = String(Math.floor((seconds % 3600) / 60)).padStart(2, '0');
        const secs = String(seconds % 60).padStart(2, '0');
        const timerEl = document.getElementById('cybroria-timer');
        if (timerEl) {
            timerEl.textContent = `⏱ since last reset: ${hrs}:${mins}:${secs}`;
        }
    }

    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 totalIncome = 0;
        const html = [];
        html.push('<strong>Cybroria Loot Tracker</strong><br><br>');
        html.push(`<u>Per ${viewMode.charAt(0).toUpperCase() + viewMode.slice(1)}:</u><br><br>`);

        const sections = {
            "Fighting Stats": ["Strength", "Agility", "Dexterity", "Vitality"],
            "Extraction Stats": ["Energy Tap", "System Breach", "Chemsynthesis", "Cyber Harvest"],
            "Currency": ["Credits", "Artifacts", "Cyber Components"],
            "Extraction Loot": ["Power Cells", "Logic Cores", "Cyber Implants", "Neuro Stims"]
        };

        for (const [title, items] of Object.entries(sections)) {
            const group = items.map(item => {
                if (!stats[item]) return null;
                const amount = stats[item];
                const value = lootValues[item] ? amount * lootValues[item] : 0;
                totalIncome += value;
                return `${item}: ${amount.toLocaleString()}`;
            }).filter(Boolean);
            if (group.length) {
                html.push(`<strong>${title}:</strong><br>${group.join('<br>')}<br><br>`);
            }
        }

        if (totalIncome > 0) {
            html.push(`<strong>Total Income:</strong> ${totalIncome.toLocaleString()} credits<br>`);
        }

        const timerDiv = document.getElementById('cybroria-timer');
        trackerBox.innerHTML = html.join('');
        if (timerDiv) trackerBox.appendChild(timerDiv);
        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 showSettingsPopup() {
        const popup = document.createElement('div');
        popup.style.position = 'fixed';
        popup.style.top = '50%';
        popup.style.left = '50%';
        popup.style.transform = 'translate(-50%, -50%)';
        popup.style.background = '#222';
        popup.style.color = '#fff';
        popup.style.padding = '20px';
        popup.style.border = '2px solid #0f0';
        popup.style.zIndex = 10000;
        popup.style.maxHeight = '80vh';
        popup.style.overflowY = 'auto';

        const closeBtn = document.createElement('button');
        closeBtn.textContent = 'Close';
        closeBtn.style.marginTop = '10px';
        closeBtn.onclick = () => popup.remove();

        const inputItems = [
            "Artifacts", "Power Cells", "Logic Cores", "Cyber Implants",
            "Neuro Stims", "Cyber Components"
        ];
        lootValues.Credits = 1;

        inputItems.forEach(item => {
            const label = document.createElement('label');
            label.textContent = `${item} price: `;
            label.style.display = 'block';

            const input = document.createElement('input');
            input.type = 'number';
            input.value = lootValues[item] || '';
            input.style.marginBottom = '6px';
            input.onchange = () => {
                lootValues[item] = parseFloat(input.value) || 0;
                localStorage.setItem(VALUE_KEY, JSON.stringify(lootValues));
                updateBox();
            };

            label.appendChild(input);
            popup.appendChild(label);
        });

        popup.appendChild(closeBtn);
        document.body.appendChild(popup);
    }

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

        const statMatch = text.match(/You have found ([\d,]+) ([A-Za-z ]+?) Stat Value/i);
        if (statMatch) {
            const amount = parseInt(statMatch[1].replace(/,/g, ''), 10);
            const statName = statMatch[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);
    }

    function trackCyberComponentDelta() {
        setInterval(() => {
            const el = Array.from(document.querySelectorAll('*')).find(e =>
                e.textContent && e.textContent.includes("Cyber Components")
            );
            if (el) {
                const match = el.textContent.match(/Cyber Components\s*:?[\s]*([\d,]+)/i);
                if (match) {
                    const current = parseInt(match[1].replace(/,/g, ''), 10);
                    const base = parseInt(localStorage.getItem(CYBER_BASE_KEY) || current);
                    const gain = current - base;
                    localStorage.setItem(CYBER_GAIN_KEY, gain.toString());
                    localStorage.setItem(CYBER_BASE_KEY, base.toString());
                    updateBox();
                }
            }
        }, 2000);
    }

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