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 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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