Torn PDA - Graffiti Rep Tracker v4.2

Floating tracker with Nerve planner and Native UI coloring (Shades of Green & Blue Leader mode).

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Torn PDA - Graffiti Rep Tracker v4.2
// @namespace    Torn.Crimes2.Graffiti.Tracker
// @version      4.2
// @description  Floating tracker with Nerve planner and Native UI coloring (Shades of Green & Blue Leader mode).
// @author       CDR.Shepard
// @license      MIT
// @match        https://www.torn.com/loader.php?sid=crimes*
// @match        https://www.torn.com/page.php?sid=crimes*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(() => {
    "use strict";

    if (document.getElementById("gt-gui")) return;

    const STORAGE_KEY = "torn_graffiti_rep_data";
    const MULTIPLIER_KEY = "torn_graffiti_multiplier";
    const INTERVAL_KEY = "torn_graffiti_interval";
    
    let isGuiClosed = false;
    let isMinimized = false;
    
    let savedMultiplier = localStorage.getItem(MULTIPLIER_KEY) || "1.0";
    let savedInterval = localStorage.getItem(INTERVAL_KEY) || "0";

    const style = document.createElement("style");
    style.textContent = `
        #gt-gui {
            position: fixed; bottom: 70px; right: 10px; width: 240px;
            border: 1px solid #333; border-radius: 8px; color: #ddd;
            font-family: Arial, sans-serif; font-size: 11px; z-index: 999999;
            box-shadow: 0px 4px 15px rgba(0,0,0,0.8); display: none;
            will-change: transform; background: rgba(15, 15, 15, 0.95);
        }
        #gt-header {
            background: #1a1a1a; padding: 8px 10px; border-bottom: 1px solid #333;
            border-radius: 8px 8px 0 0; display: flex; justify-content: space-between;
            align-items: center; cursor: grab; touch-action: none;
        }
        #gt-header:active { cursor: grabbing; }
        .gt-title { font-weight: bold; color: #e0ac16; font-size: 12px; pointer-events: none; }
        .gt-controls { display: flex; gap: 4px; }
        .gt-controls button {
            background: #333; color: #fff; border: none; width: 22px; height: 22px; 
            border-radius: 4px; cursor: pointer; font-weight: bold; line-height: 1;
        }
        .gt-controls button:active { background: #555; }
        #gt-body { padding: 10px; border-radius: 0 0 8px 8px; }
        .gt-settings-row {
            display: flex; justify-content: space-between; align-items: center; gap: 5px;
            background: #1a1a1a; padding: 4px 8px; border-radius: 4px; border: 1px solid #2a2a2a;
            margin-bottom: 8px;
        }
        .gt-settings-row input {
            width: 40px; background: #000; color: #44ff44; border: 1px solid #444;
            border-radius: 3px; text-align: center; font-size: 11px; padding: 2px;
        }
        .gt-timestamp { font-size: 10px; color: #aaa; text-align: center; margin-bottom: 8px; }
        .gt-status { font-size: 11px; text-align: center; margin-bottom: 8px; font-weight: bold; height: 12px; }
        .gt-grid-header {
            display: flex; justify-content: space-between; padding-bottom: 4px;
            border-bottom: 1px solid #444; font-weight: bold; color: #fff; margin-bottom: 4px;
        }
        .gt-col-1 { flex: 2; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
        .gt-col-2 { flex: 1.5; text-align: right; display: flex; justify-content: flex-end; align-items: center; }
        .gt-col-3 { flex: 1; text-align: right; }
        .gt-data-container {
            max-height: 150px; overflow-y: auto; margin-bottom: 10px;
            scrollbar-width: thin; scrollbar-color: #555 #222;
        }
        .gt-area-row { display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid #2a2a2a; }
        .gt-locked { color: #ff4444; font-style: italic; }
        .gt-rep { color: #44ff44; font-weight: bold; }
        .gt-delta-pos { color: #44ff44; font-size: 9px; margin-left: 4px; font-weight: normal; }
        .gt-delta-neg { color: #ff4444; font-size: 9px; margin-left: 4px; font-weight: normal; }
        .gt-delta-zero { color: #ffff44; font-size: 9px; margin-left: 4px; font-weight: normal; }
        .gt-nerve-est { color: #ff9900; }
        .gt-nerve-done { color: #aaa; }
        .gt-btn-fetch {
            background: #1e4d1e; color: #fff; border: 1px solid #2e7d2e; padding: 8px;
            border-radius: 4px; cursor: pointer; width: 100%; font-weight: bold; font-size: 11px;
        }
        .gt-btn-fetch:active { background: #2e7d2e; }
    `;
    document.head.appendChild(style);

    const gui = document.createElement("div");
    gui.id = "gt-gui";
    gui.innerHTML = `
        <div id="gt-header">
            <span class="gt-title">Graffiti Rep v4.2</span>
            <div class="gt-controls">
                <button id="gt-min-btn">-</button>
                <button id="gt-close-btn">X</button>
            </div>
        </div>
        <div id="gt-body">
            <div class="gt-settings-row">
                <div><span style="color: #aaa;">Avg Rep: </span><input type="number" id="gt-multiplier" value="${savedMultiplier}" step="0.1" min="0.1"></div>
                <div><span style="color: #aaa;">Interval: </span><input type="number" id="gt-interval" value="${savedInterval}" step="1" min="0"></div>
            </div>
            <div class="gt-timestamp" id="gt-time">No data fetched yet</div>
            <div class="gt-status" id="gt-status"></div>
            <div class="gt-grid-header">
                <span class="gt-col-1">Area</span>
                <span class="gt-col-2">Rep (Δ)</span>
                <span class="gt-col-3">Nerve</span>
            </div>
            <div class="gt-data-container" id="gt-list"></div>
            <button class="gt-btn-fetch" id="gt-fetch-btn">FETCH DATA</button>
        </div>
    `;
    document.body.appendChild(gui);

    const timeDisplay = document.getElementById("gt-time");
    const listDisplay = document.getElementById("gt-list");
    const statusDisplay = document.getElementById("gt-status");
    const multInput = document.getElementById("gt-multiplier");
    const intInput = document.getElementById("gt-interval");
    const header = document.getElementById("gt-header");
    const body = document.getElementById("gt-body");

    multInput.addEventListener("input", () => {
        let val = parseFloat(multInput.value);
        if (!isNaN(val) && val > 0) {
            localStorage.setItem(MULTIPLIER_KEY, val.toString());
            loadSavedData();
        }
    });

    intInput.addEventListener("input", () => {
        let val = parseInt(intInput.value, 10);
        if (!isNaN(val) && val >= 0) {
            localStorage.setItem(INTERVAL_KEY, val.toString());
            applyNativeStyling();
        }
    });

    function flashStatus(msg, isError) {
        statusDisplay.innerText = msg;
        statusDisplay.style.color = isError ? "#ff4444" : "#44ff44";
        setTimeout(() => { statusDisplay.innerText = ""; }, 3000);
    }

    function calculateNerve(repNum, isLocked, mult) {
        if (isLocked) return `<span class="gt-nerve-est">~${Math.ceil((500 / mult) * 3)}</span>`;
        if (repNum >= 500) return `<span class="gt-nerve-done">Done</span>`;
        return `<span class="gt-nerve-est">~${Math.ceil(((500 - repNum) / mult) * 3)}</span>`;
    }

    function loadSavedData() {
        const saved = localStorage.getItem(STORAGE_KEY);
        if (saved) {
            try {
                const data = JSON.parse(saved);
                timeDisplay.innerText = `Last Fetch: ${data.timestamp}`;
                let mult = parseFloat(multInput.value) || 1.0;
                if (mult <= 0) mult = 0.1;
                
                listDisplay.innerHTML = "";
                data.areas.forEach(area => {
                    const row = document.createElement("div");
                    row.className = "gt-area-row";
                    const isLocked = area.rep.includes("Locked");
                    
                    const col1 = document.createElement("span");
                    col1.className = "gt-col-1";
                    col1.innerText = area.name;
                    
                    const col2 = document.createElement("span");
                    col2.className = "gt-col-2";
                    const repSpan = document.createElement("span");
                    repSpan.className = isLocked ? "gt-locked" : "gt-rep";
                    repSpan.innerText = area.rep;
                    col2.appendChild(repSpan);

                    if (area.hasOldData) {
                        const deltaSpan = document.createElement("span");
                        if (area.delta > 0) {
                            deltaSpan.className = "gt-delta-pos"; deltaSpan.innerText = `(+${area.delta})`;
                        } else if (area.delta < 0) {
                            deltaSpan.className = "gt-delta-neg"; deltaSpan.innerText = `(${area.delta})`;
                        } else {
                            deltaSpan.className = "gt-delta-zero"; deltaSpan.innerText = `(0)`;
                        }
                        col2.appendChild(deltaSpan);
                    }

                    const col3 = document.createElement("span");
                    col3.className = "gt-col-3";
                    col3.innerHTML = calculateNerve(area.rawRep, isLocked, mult);
                    
                    row.appendChild(col1); row.appendChild(col2); row.appendChild(col3);
                    listDisplay.appendChild(row);
                });
            } catch (e) { console.error("Graffiti Tracker: Error parsing data."); }
        }
    }

    function applyNativeStyling() {
        const saved = localStorage.getItem(STORAGE_KEY);
        if (!saved) return;
        
        let data;
        try { data = JSON.parse(saved); } catch (e) { return; }
        
        if (!data || !Array.isArray(data.areas)) return;
        
        const interval = parseInt(intInput.value, 10) || 0;
        
        let highestActiveRep = 0;
        data.areas.forEach(a => {
            if (a.rawRep < 500 && a.rawRep > highestActiveRep) {
                highestActiveRep = a.rawRep;
            }
        });

        const cards = document.querySelectorAll('[class*="crimeWrapper"], [class*="crimeOption"], [class*="crimePanel"]');
        
        cards.forEach(card => {
            const lines = card.innerText.split('\n').map(line => line.trim()).filter(line => line !== "");
            if (lines.length < 2) return;
            const areaName = lines[0];
            
            const matchArea = data.areas.find(a => a.name === areaName);
            if (!matchArea) return;
            
            const rep = matchArea.rawRep;
            if (rep === 0 || matchArea.rep === "Locked") return;

            const rowWrapper = card.querySelector('[class*="item"], [class*="row"]') || card.firstElementChild;
            if (!rowWrapper) return;

            const children = Array.from(rowWrapper.children);

            let boxA = children.find(child => child.querySelector('svg') || (child.querySelector('img') && !child.querySelector('img').src.includes('nerve') && !child.querySelector('img').src.includes('money')));
            
            if (!boxA && children.length >= 2) { boxA = children[1]; }

            if (children[0] && children[0] !== boxA) { children[0].style.backgroundColor = ''; }

            if (boxA) {
                boxA.style.transition = 'background 0.3s';
                boxA.style.borderRadius = '4px';
                boxA.style.padding = '2px';
                if (rep >= 500) {
                    boxA.style.backgroundColor = 'rgba(255, 255, 0, 0.4)'; 
                } else if (interval === 0) {
                    boxA.style.backgroundColor = (rep === highestActiveRep) ? 'rgba(0, 0, 255, 0.4)' : 'rgba(0, 128, 0, 0.4)';
                } else {
                    const target = highestActiveRep + interval;
                    const opacity = Math.min(0.8, Math.max(0.1, rep / target));
                    boxA.style.backgroundColor = `rgba(0, 128, 0, ${opacity})`;
                }
            }
        });
    }

    function fetchReputation() {
        const cards = document.querySelectorAll('[class*="crimeWrapper"], [class*="crimeOption"], [class*="crimePanel"]');
        if (cards.length === 0) {
            flashStatus("Wait for areas to load...", true);
            return;
        }

        const oldSaved = localStorage.getItem(STORAGE_KEY);
        let oldData = null;
        try { oldData = oldSaved ? JSON.parse(oldSaved) : null; } catch(e) { oldData = null; }
        
        const oldRepMap = {};
        
        if (oldData && oldData.areas) {
            oldData.areas.forEach(a => {
                let oldRepNum = a.rawRep !== undefined ? a.rawRep : parseInt(a.rep, 10);
                if (isNaN(oldRepNum)) oldRepNum = 0;
                oldRepMap[a.name] = oldRepNum;
            });
        }

        const areas = [];
        const seenNames = new Set();

        cards.forEach(card => {
            const lines = card.innerText.split('\n').map(line => line.trim()).filter(line => line !== "");
            if (lines.length >= 2) {
                const areaName = lines[0];
                const repValueStr = lines[1];
                const startsWithNumber = /^\d/.test(areaName);
                
                if (!startsWithNumber && !seenNames.has(areaName)) {
                    seenNames.add(areaName);
                    
                    let newRepNum = parseInt(repValueStr, 10);
                    if (isNaN(newRepNum)) newRepNum = 0;
                    
                    let finalStatus = newRepNum.toString();
                    if (repValueStr === "0" || repValueStr.includes("Locked") || isNaN(parseInt(repValueStr, 10))) {
                        finalStatus = "Locked";
                        newRepNum = 0;
                    }

                    let delta = 0;
                    let hasOldData = oldRepMap.hasOwnProperty(areaName);
                    if (hasOldData) { delta = newRepNum - oldRepMap[areaName]; }

                    areas.push({ name: areaName, rep: finalStatus, rawRep: newRepNum, delta: delta, hasOldData: hasOldData });
                }
            }
        });

        if (areas.length > 0) {
            const options = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true };
            const timestampLocal = new Date().toLocaleString(undefined, options);

            localStorage.setItem(STORAGE_KEY, JSON.stringify({ timestamp: timestampLocal, areas: areas }));
            loadSavedData();
            applyNativeStyling();
            flashStatus("Success!", false);
        } else {
            flashStatus("Failed to read text.", true);
        }
    }

    document.getElementById("gt-fetch-btn").addEventListener("click", fetchReputation);
    document.getElementById("gt-close-btn").addEventListener("click", () => {
        isGuiClosed = true;
        gui.style.display = "none";
    });
    document.getElementById("gt-min-btn").addEventListener("click", () => {
        isMinimized = !isMinimized;
        body.style.display = isMinimized ? "none" : "block";
        document.getElementById("gt-min-btn").innerText = isMinimized ? "+" : "-";
        if (isMinimized) header.style.borderRadius = "8px"; 
        else header.style.borderRadius = "8px 8px 0 0";
    });

    let isDragging = false, currentX, currentY, initialX, initialY, xOffset = 0, yOffset = 0;
    function dragStart(e) {
        if (e.target.tagName === "BUTTON" || e.target.tagName === "INPUT") return; 
        if (e.type === "touchstart") {
            initialX = e.touches[0].clientX - xOffset;
            initialY = e.touches[0].clientY - yOffset;
        } else {
            initialX = e.clientX - xOffset;
            initialY = e.clientY - yOffset;
        }
        isDragging = true;
    }
    function dragEnd() { initialX = currentX; initialY = currentY; isDragging = false; }
    function drag(e) {
        if (!isDragging) return;
        e.preventDefault(); 
        if (e.type === "touchmove") {
            currentX = e.touches[0].clientX - initialX;
            currentY = e.touches[0].clientY - initialY;
        } else {
            currentX = e.clientX - initialX;
            currentY = e.clientY - initialY;
        }
        xOffset = currentX; yOffset = currentY;
        gui.style.transform = `translate3d(${currentX}px, ${currentY}px, 0)`;
    }

    header.addEventListener("touchstart", dragStart, { passive: false });
    document.addEventListener("touchend", dragEnd);
    document.addEventListener("touchmove", drag, { passive: false });
    header.addEventListener("mousedown", dragStart);
    document.addEventListener("mouseup", dragEnd);
    document.addEventListener("mousemove", drag);

    setInterval(() => {
        const url = window.location.href;
        const isGraffitiPage = url.includes("crimes") && url.includes("graffiti");
        
        if (isGraffitiPage) {
            if (!isGuiClosed && gui.style.display !== "block") {
                loadSavedData();
                gui.style.display = "block";
            }
            applyNativeStyling(); 
        } else {
            gui.style.display = "none";
            if (!url.includes("crimes")) isGuiClosed = false; 
        }
    }, 1000);

})();