Cybroria Loot Tracker v4.3 (Complete)

Tracks loot, stats, Cyber Components, with theme switcher, keyboard shortcut, opacity control, click-through toggle, and collapsible controls.

目前為 2025-05-12 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Cybroria Loot Tracker v4.3 (Complete)
// @namespace    http://tampermonkey.net/
// @version      4.3
// @description  Tracks loot, stats, Cyber Components, with theme switcher, keyboard shortcut, opacity control, click-through toggle, and collapsible controls.
// @author       Skydragon
// @match        https://cybroria.com/*
// @grant        none
// @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';
    const OPACITY_KEY = 'cybroria_tracker_opacity';
    const THEME_KEY = 'cybroria_tracker_theme';
    const CLICKTHRU_KEY = 'cybroria_tracker_clickthrough';
    const UI_COLLAPSE_KEY = 'cybroria_tracker_controls_hidden';

    let timestamps = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
    let lootValues = JSON.parse(localStorage.getItem(VALUE_KEY) || '{}');
    let viewMode = 'hour';
    let panelOpacity = parseFloat(localStorage.getItem(OPACITY_KEY)) || 0.6;
    let theme = localStorage.getItem(THEME_KEY) || 'dark';
    let clickThrough = localStorage.getItem(CLICKTHRU_KEY) === 'true';
    let controlsHidden = localStorage.getItem(UI_COLLAPSE_KEY) === 'true';

    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"
    ];

    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 trackerBox = document.createElement('div');
    trackerBox.id = 'cybroria-tracker';
    trackerBox.style.position = 'fixed';
    trackerBox.style.top = localStorage.getItem(POS_KEY + '_top') || '10px';
    trackerBox.style.left = localStorage.getItem(POS_KEY + '_left') || '10px';
    applyPanelStyles();

    function applyPanelStyles() {
        const base = trackerBox.style;
        const glow = '0 0 12px rgba(0,255,100,0.4)';
        const light = '0 0 8px rgba(255,255,255,0.3)';
        base.background = theme === 'dark' ? `rgba(0, 0, 0, ${panelOpacity})` : `rgba(255, 255, 255, ${panelOpacity})`;
        base.color = theme === 'dark' ? '#cfc' : '#333';
        base.borderRadius = '10px';
        base.boxShadow = theme === 'dark' ? glow : light;
        base.border = theme === 'dark' ? '1px solid rgba(0,255,100,0.3)' : '1px solid rgba(0,0,0,0.2)';
        base.textShadow = theme === 'dark' ? '0 0 4px rgba(0,255,100,0.3)' : 'none';
        base.fontFamily = 'monospace';
        base.fontSize = '13px';
        base.padding = '12px';
        base.zIndex = 9999;
        base.cursor = 'move';
        base.minWidth = '240px';
        base.pointerEvents = clickThrough ? 'none' : 'auto';
    }

    document.body.appendChild(trackerBox);

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

    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;
        });
    }

    makeDraggable(trackerBox);

    // Ctrl+L to toggle visibility
    window.addEventListener('keydown', (e) => {
        if (e.ctrlKey && e.key.toLowerCase() === 'l') {
            trackerBox.style.display = trackerBox.style.display === 'none' ? 'block' : 'none';
        }
    });
    function renderControls() {
        const controlsWrapper = document.createElement('div');
        controlsWrapper.id = 'tracker-controls';

        const collapseToggle = document.createElement('div');
        collapseToggle.textContent = controlsHidden ? '[+]' : '[–]';
        collapseToggle.style.cursor = 'pointer';
        collapseToggle.style.marginBottom = '6px';
        collapseToggle.style.fontWeight = 'bold';
        collapseToggle.onclick = () => {
            controlsHidden = !controlsHidden;
            localStorage.setItem(UI_COLLAPSE_KEY, controlsHidden);
            updateBox();
        };
        trackerBox.appendChild(collapseToggle);

        if (controlsHidden) return;

        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;

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

    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}`;
        }
    }

    setInterval(updateTimer, 1000);
    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);
        });

        // Opacity Slider
        const opacityLabel = document.createElement('label');
        opacityLabel.textContent = `Panel Opacity: ${Math.round(panelOpacity * 100)}%`;
        opacityLabel.style.display = 'block';
        const opacitySlider = document.createElement('input');
        opacitySlider.type = 'range';
        opacitySlider.min = '0.1';
        opacitySlider.max = '1';
        opacitySlider.step = '0.05';
        opacitySlider.value = panelOpacity;
        opacitySlider.oninput = () => {
            panelOpacity = parseFloat(opacitySlider.value);
            localStorage.setItem(OPACITY_KEY, panelOpacity);
            opacityLabel.textContent = `Panel Opacity: ${Math.round(panelOpacity * 100)}%`;
            applyPanelStyles();
        };
        opacityLabel.appendChild(opacitySlider);
        popup.appendChild(opacityLabel);

        // Theme Switch
        const themeLabel = document.createElement('label');
        themeLabel.textContent = `Theme: `;
        themeLabel.style.display = 'block';
        const themeSelect = document.createElement('select');
        ['dark', 'light'].forEach(t => {
            const opt = document.createElement('option');
            opt.value = t;
            opt.textContent = t.charAt(0).toUpperCase() + t.slice(1);
            themeSelect.appendChild(opt);
        });
        themeSelect.value = theme;
        themeSelect.onchange = () => {
            theme = themeSelect.value;
            localStorage.setItem(THEME_KEY, theme);
            applyPanelStyles();
            updateBox();
        };
        themeLabel.appendChild(themeSelect);
        popup.appendChild(themeLabel);

        // Click-through toggle
        const clickBox = document.createElement('input');
        clickBox.type = 'checkbox';
        clickBox.checked = clickThrough;
        clickBox.onchange = () => {
            clickThrough = clickBox.checked;
            localStorage.setItem(CLICKTHRU_KEY, clickThrough);
            applyPanelStyles();
        };
        const clickLabel = document.createElement('label');
        clickLabel.textContent = ' Enable Click-Through Mode';
        clickLabel.prepend(clickBox);
        clickLabel.style.display = 'block';
        popup.appendChild(clickLabel);

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

    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>`);
        }

        trackerBox.innerHTML = html.join('');
        trackerBox.appendChild(timer);
        renderControls();
    }

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