OC 2.0

Simplifies Organized Crimes including total time-spent in OC's for members each year.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         OC 2.0
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Simplifies Organized Crimes including total time-spent in OC's for members each year.
// @author       Robert Pinney
// @match        https://www.torn.com/factions.php*
// @grant        none
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    const API_KEY = 'YOUR_API_KEY_HERE'; // limited access API key is sufficient

    let crimesData = {};
    let membersData = {};
    let timeSpentData = {}; // Store time spent data

    // Fetch organized crimes
    function fetchFactionCrimes() {
        fetch(`https://api.torn.com/v2/faction/crimes?key=${API_KEY}`)
            .then((response) => response.json())
            .then((data) => {
                if (data.error) {
                    console.error('API Error (Crimes):', data.error.error);
                } else {
                    crimesData = data.crimes;
                    calculateTimeSpentInOCs(crimesData); // Calculate time spent in OCs
                    displayTabContent('members'); // Default to Members tab
                }
            })
            .catch((err) => console.error('API Fetch Error (Crimes):', err));
    }

    // Fetch faction members
    function fetchFactionMembers() {
        fetch(`https://api.torn.com/v2/faction/members?key=${API_KEY}`)
            .then((response) => response.json())
            .then((data) => {
                if (data.error) {
                    console.error('API Error (Members):', data.error.error);
                } else {
                    membersData = data.members;
                }
            })
            .catch((err) => console.error('API Fetch Error (Members):', err));
    }

    // Calculate time spent in OCs
    function calculateTimeSpentInOCs(crimes) {
        timeSpentData = {}; // Reset timeSpentData
        Object.values(crimes).forEach((crime) => {
            if (crime.status === 'Successful' && crime.executed_at) {
                crime.slots.forEach((slot) => {
                    const memberId = slot.user?.id;
                    const joinedAt = slot.user?.joined_at;

                    if (memberId && joinedAt) {
                        const timeInCrime = crime.executed_at - joinedAt;
                        if (!isNaN(timeInCrime)) {
                            if (!timeSpentData[memberId]) {
                                timeSpentData[memberId] = 0;
                            }
                            timeSpentData[memberId] += timeInCrime;
                        }
                    }
                });
            }
        });
    }

    // Format time spent (in seconds) as d/h/m
    function formatTimeSpent(seconds) {
        if (!seconds || seconds <= 0) return '0m';

        const days = Math.floor(seconds / 86400);
        const hours = Math.floor((seconds % 86400) / 3600);
        const mins = Math.floor((seconds % 3600) / 60);

        let parts = [];
        if (days > 0) parts.push(`${days}d`);
        if (hours > 0) parts.push(`${hours}h`);
        if (mins > 0) parts.push(`${mins}m`);

        return parts.length ? parts.join(' ') : '0m';
    }

    // [ADDED] Small helper to build a styled container
    function wrapContentInContainer(innerHTML) {
        return `
            <div style="
                padding: 10px;
                color: #fff;             /* Bright text */
                font-size: 14px;
            ">
                ${innerHTML}
            </div>
        `;
    }

    // Generate content for each tab
    function generateMembersContent() {
        if (!membersData || Object.keys(membersData).length === 0) {
            return wrapContentInContainer('<p>No members data available.</p>');
        }

        const unassigned = [];
        const assigned = [];
        Object.values(membersData).forEach((member) => {
            if (member.is_in_oc) {
                assigned.push(member);
            } else {
                unassigned.push(member);
            }
        });

        // Helper to generate a table
        const createTable = (membersArray) => {
            return `
                <table style="
                    width: 100%;
                    border-collapse: collapse;
                    margin: 10px 0;
                ">
                  <thead>
                    <tr>
                      <th style="
                          background-color: #555;
                          color: #fff;
                          text-align: left;
                          padding: 8px;
                          border-bottom: 1px solid #777;
                      ">Name</th>
                      <th style="
                          background-color: #555;
                          color: #fff;
                          text-align: left;
                          padding: 8px;
                          border-bottom: 1px solid #777;
                      ">Level</th>
                      <th style="
                          background-color: #555;
                          color: #fff;
                          text-align: left;
                          padding: 8px;
                          border-bottom: 1px solid #777;
                      ">Time in OCs</th>
                    </tr>
                  </thead>
                  <tbody>
                    ${membersArray.map((member) => {
                        const memberId = parseInt(member.id, 10);
                        const timeSpent = timeSpentData[memberId]
                            ? formatTimeSpent(timeSpentData[memberId])
                            : '0m';
                        return `
                            <tr>
                              <td style="
                                  background-color: #444;
                                  color: #fff;
                                  padding: 8px;
                                  border-bottom: 1px solid #555;
                              ">${member.name}</td>
                              <td style="
                                  background-color: #444;
                                  color: #fff;
                                  padding: 8px;
                                  border-bottom: 1px solid #555;
                              ">${member.level}</td>
                              <td style="
                                  background-color: #444;
                                  color: #fff;
                                  padding: 8px;
                                  border-bottom: 1px solid #555;
                              ">${timeSpent}</td>
                            </tr>
                        `;
                    }).join('')}
                  </tbody>
                </table>
            `;
        };

        let html = `
            <h3 style="border-bottom: 1px solid #fff; padding-bottom: 5px;">Unassigned</h3>
            ${unassigned.length === 0
                ? '<p>No unassigned members.</p>'
                : createTable(unassigned)
            }

            <h3 style="border-bottom: 1px solid #fff; padding-bottom: 5px; margin-top: 15px;">Assigned</h3>
            ${assigned.length === 0
                ? '<p>No assigned members.</p>'
                : createTable(assigned)
            }
        `;
        return wrapContentInContainer(html);
    }

    function generateRecruitingContent() {
        const recruitingCrimes = Object.values(crimesData).filter(
            (crime) => crime.status === 'Recruiting'
        );
        if (recruitingCrimes.length === 0) {
            return wrapContentInContainer('<p>No crimes currently recruiting.</p>');
        }

        const summary = {};
        recruitingCrimes.forEach((crime) => {
            const level = crime.difficulty;
            const totalSlots = crime.slots.length;
            const remainingSlots = crime.slots.filter((slot) => !slot.user).length;
            if (!summary[level]) {
                summary[level] = { count: 0, remainingSlots: 0, totalSlots: 0 };
            }
            summary[level].count++;
            summary[level].remainingSlots += remainingSlots;
            summary[level].totalSlots += totalSlots;
        });

        const sorted = Object.entries(summary).sort((a, b) => b[0] - a[0]);
        let listItems = sorted.map(([level, data]) =>
            `${data.count} level ${level}'s: ${data.remainingSlots} / ${data.totalSlots} slots remaining`
        );

        // [UPDATED STYLE] Show these items in a styled list
        let html = `
            <ul style="list-style-type: none; padding: 0; margin: 0;">
                ${listItems.map(item => `
                    <li style="
                        background-color: #444;
                        margin-bottom: 5px;
                        padding: 8px;
                        color: #fff;
                        border: 1px solid #555;
                        border-radius: 3px;
                    ">${item}</li>
                `).join('')}
            </ul>
        `;
        return wrapContentInContainer(html);
    }

    function generatePlanningContent() {
        const planningCrimes = Object.values(crimesData).filter(
            (crime) => crime.status === 'Planning'
        );
        if (planningCrimes.length === 0) {
            return wrapContentInContainer('<p>No crimes currently being planned.</p>');
        }

        planningCrimes.sort((a, b) => a.ready_at - b.ready_at);
        let listItems = planningCrimes.map((crime) => {
            const timeRemaining = crime.ready_at - Math.floor(Date.now() / 1000);
            return `${formatTimeRemaining(timeRemaining)} - level ${crime.difficulty}`;
        });

        // [UPDATED STYLE] Show these items in a styled list
        let html = `
            <ul style="list-style-type: none; padding: 0; margin: 0;">
                ${listItems.map(item => `
                    <li style="
                        background-color: #444;
                        margin-bottom: 5px;
                        padding: 8px;
                        color: #fff;
                        border: 1px solid #555;
                        border-radius: 3px;
                    ">${item}</li>
                `).join('')}
            </ul>
        `;
        return wrapContentInContainer(html);
    }

    function generateCompletedContent() {
        const oneYearAgo = Math.floor(Date.now() / 1000) - 365 * 24 * 60 * 60;
        const completedCrimes = Object.values(crimesData).filter(
            (crime) => crime.status === 'Successful' && crime.executed_at >= oneYearAgo
        );
        if (completedCrimes.length === 0) {
            return wrapContentInContainer('<p>No crimes completed in the last year.</p>');
        }

        completedCrimes.sort((a, b) => b.executed_at - a.executed_at);
        let listItems = completedCrimes.map((crime) => {
            return `${formatDate(crime.executed_at)} - level ${crime.difficulty}`;
        });

        // [UPDATED STYLE] Show these items in a styled list
        let html = `
            <ul style="list-style-type: none; padding: 0; margin: 0;">
                ${listItems.map(item => `
                    <li style="
                        background-color: #444;
                        margin-bottom: 5px;
                        padding: 8px;
                        color: #fff;
                        border: 1px solid #555;
                        border-radius: 3px;
                    ">${item}</li>
                `).join('')}
            </ul>
        `;
        return wrapContentInContainer(html);
    }

    // Helper to format time remaining
    function formatTimeRemaining(seconds) {
        const minutes = Math.floor(seconds / 60);
        const hours = Math.floor(minutes / 60);
        const days = Math.floor(hours / 24);
        if (days > 0) return `${days}d ${hours % 24}h ${minutes % 60}m`;
        if (hours > 0) return `${hours}h ${minutes % 60}m`;
        return `${minutes}m`;
    }

    // Helper to format date
    function formatDate(timestamp) {
        const date = new Date(timestamp * 1000);
        return date.toLocaleString();
    }

    // Create the OC Window
    function createOCWindow() {
        const windowEl = document.createElement('div');
        windowEl.id = 'oc-window';
        // [UPDATED STYLE]
        windowEl.style.position = 'fixed';
        windowEl.style.top = '100px';
        windowEl.style.right = '20px';
        windowEl.style.width = '350px';
        windowEl.style.backgroundColor = '#333';
        windowEl.style.color = '#fff';
        windowEl.style.padding = '10px';
        windowEl.style.borderRadius = '5px';
        windowEl.style.zIndex = '1000';
        windowEl.style.display = 'none';

        const tabs = document.createElement('div');
        tabs.style.display = 'flex';
        tabs.style.marginBottom = '10px';
        ['members', 'recruiting', 'planning', 'completed'].forEach((tab) => {
            const tabButton = document.createElement('button');
            tabButton.textContent = tab.charAt(0).toUpperCase() + tab.slice(1);
            // [UPDATED STYLE]
            tabButton.style.flex = '1';
            tabButton.style.backgroundColor = '#444';
            tabButton.style.color = '#fff';
            tabButton.style.padding = '8px';
            tabButton.style.cursor = 'pointer';
            tabButton.style.marginRight = '5px';
            tabButton.style.border = '1px solid #555';
            tabButton.style.borderRadius = '3px';

            tabButton.addEventListener('click', () => displayTabContent(tab));
            tabs.appendChild(tabButton);
        });

        const content = document.createElement('div');
        content.id = 'oc-content';
        // [UPDATED STYLE]
        content.style.maxHeight = '400px';
        content.style.overflowY = 'auto';
        content.style.borderTop = '1px solid #555';
        content.style.paddingTop = '10px';

        windowEl.appendChild(tabs);
        windowEl.appendChild(content);
        document.body.appendChild(windowEl);
    }

    // Toggle OC Window
    function toggleOCWindow() {
        const windowEl = document.getElementById('oc-window');
        if (windowEl) {
            windowEl.style.display = windowEl.style.display === 'none' ? 'block' : 'none';
        }
    }

    function createToggleButton() {
        // Attempt to find an existing Faction Warfare link
        const factionWarfareLink = document.querySelector('a[href="/page.php?sid=factionWarfare"]');
        if (factionWarfareLink) {
            const toggleLink = document.createElement('a');
            toggleLink.textContent = 'OC Simple';
            toggleLink.href = '#';
            // [UPDATED STYLE]
            toggleLink.style.color = '#fff';
            toggleLink.style.backgroundColor = '#444';
            toggleLink.style.padding = '5px 10px';
            toggleLink.style.textDecoration = 'none';
            toggleLink.style.borderRadius = '5px';
            toggleLink.style.marginLeft = '10px';
            toggleLink.style.display = 'inline-block';
            toggleLink.style.verticalAlign = 'middle';
            toggleLink.style.cursor = 'pointer';
            toggleLink.style.border = '1px solid #555';

            toggleLink.addEventListener('click', (e) => {
                e.preventDefault();
                toggleOCWindow();
            });

            // Insert the button next to "Faction Warfare"
            factionWarfareLink.parentNode.insertBefore(toggleLink, factionWarfareLink.nextSibling);
        }
    }

    function displayTabContent(tab) {
        const contentContainer = document.getElementById('oc-content');
        if (!contentContainer) return;

        let content = '';
        if (tab === 'members') content = generateMembersContent();
        else if (tab === 'recruiting') content = generateRecruitingContent();
        else if (tab === 'planning') content = generatePlanningContent();
        else if (tab === 'completed') content = generateCompletedContent();
        else content = `<p>Content for the "${tab}" tab is not yet implemented.</p>`;

        contentContainer.innerHTML = content;
    }

    function init() {
        createToggleButton();
        createOCWindow();
        fetchFactionMembers();
        fetchFactionCrimes();
    }

    init();
})();