OCFacilitation

Make OC 2.0 easier for regular players

// ==UserScript==
// @name         OCFacilitation
// @name:zh-CN   OC协助工具
// @namespace    https://gf.qytechs.cn/users/[daluo]
// @version      1.0.2.3
// @description  Make OC 2.0 easier for regular players
// @description:zh-CN  使普通玩家oc2.0更简单和方便
// @author       daluo
// @match        https://www.torn.com/*
// @license      MIT
// @grant        none
// ==/UserScript==
(function() {
    'use strict';
    const APIKey = "不使用冰蛙的大佬,替换成自己的apiKey,limit就可以";
    // =============== 配置管理 ===============
    const CONFIG = {
        API: {
            KEY: (() => {
                try {
                    // 尝试多种方式获取API Key
                    return localStorage.getItem("APIKey") || 
                           GM_getValue("APIKey")
                } catch (error) {
                    console.error('获取API Key失败:', error);
                    return APIKey
                }
            })(),
            URL: 'https://api.torn.com/v2/faction/crimes',
            PARAMS: { CATEGORY: 'available' }
        },
        UI: {
            LOAD_DELAY: 300,
            UPDATE_DEBOUNCE: 500,
            TIME_TOLERANCE: 2,
            SELECTORS: {
                WRAPPER: '.wrapper___U2Ap7',
                SLOTS: '.wrapper___Lpz_D',
                WAITING: '.waitingJoin___jq10k',
                TITLE: '.title___pB5FU',
                PANEL_TITLE: '.panelTitle___aoGuV',
                MOBILE_INFO: '.user-information-mobile___WjXnd'
            },
            STYLES: {
                URGENT: {
                    BORDER: '3px solid red',
                    COLOR: 'red'
                },
                STABLE: {
                    BORDER: '3px solid green',
                    COLOR: 'green'
                },
                EXCESS: {
                    BORDER: '3px solid yellow',
                    COLOR: 'blue'
                }
            }
        },
        TIME: {
            SECONDS_PER_DAY: 86400,
            HOURS_PER_DAY: 24,
            URGENT_THRESHOLD: 12,
            STABLE_THRESHOLD: 36
        }
    };

    // =============== 工具类 ===============
    class Utils {
        /**
         * 获取当前页签名称
         * @returns {string|null} 页签名称
         */
        static getCurrentTab() {
            const match = window.location.hash.match(/#\/tab=([^&]*)/);
            return match ? match[1] : null;
        }

        /**
         * 检查当前页面是否为OC页面
         * @returns {boolean}
         */
        static isOCPage() {
            return this.getCurrentTab() === 'crimes';
        }

        /**
         * 检查是否为移动端
         * @returns {boolean}
         */
        static isMobileDevice() {
            return document.querySelector(CONFIG.UI.SELECTORS.MOBILE_INFO) !== null;
        }

        /**
         * 获取当前时间戳(秒)
         * @returns {number}
         */
        static getNow() {
            return Math.floor(Date.now() / 1000);
        }

        /**
         * 防抖函数
         * @param {Function} func - 需要防抖的函数
         * @param {number} wait - 等待时间(毫秒)
         */
        static debounce(func, wait) {
            let timeout;
            return function executedFunction(...args) {
                clearTimeout(timeout);
                timeout = setTimeout(() => func.apply(this, args), wait);
            };
        }

        /**
         * 检查URL是否包含factions.php
         * @returns {boolean} 是否为faction页面
         */
        static isFactionPage() {
            return window.location.pathname === '/factions.php';
        }

        static waitForElement(selector) {
            return new Promise(resolve => {
                const element = document.querySelector(selector);
                if (element) return resolve(element);

                const observer = new MutationObserver(mutations => {
                    const element = document.querySelector(selector);
                    if (element) {
                        observer.disconnect();
                        resolve(element);
                    }
                });

                observer.observe(document.body, {
                    childList: true,
                    subtree: true
                });
            });
        }

        static calculateTimeFromParts(days, hours, minutes, seconds) {
            return (days * CONFIG.TIME.SECONDS_PER_DAY) + 
                   (hours * 3600) + 
                   (minutes * 60) + 
                   seconds;
        }

        static async waitForWrapper() {
            const maxAttempts = 10;
            const interval = 1000; // 1秒

            for (let attempts = 0; attempts < maxAttempts; attempts++) {
                const wrapper = document.querySelector(CONFIG.UI.SELECTORS.WRAPPER);
                if (wrapper?.parentNode) {
                    return wrapper.parentNode;
                }
                await Utils.delay(interval);
            }
            throw new Error('无法找到wrapper元素');
        }

        static delay(ms) {
            return new Promise(resolve => setTimeout(resolve, ms));
        }
    }

    // =============== 数据模型 ===============
    /**
     * 任务物品需求类
     */
    class ItemRequirement {
        constructor(data) {
            this.id = data.id;
            this.is_reusable = data.is_reusable;
            this.is_available = data.is_available;
        }
    }

    /**
     * 用户信息类
     */
    class User {
        constructor(data) {
            if (!data) return null;
            this.id = data.id;
            this.joined_at = data.joined_at;
        }
    }

    /**
     * 任务槽位类
     */
    class Slot {
        constructor(data) {
            this.position = data.position;
            this.item_requirement = data.item_requirement ? new ItemRequirement(data.item_requirement) : null;
            this.user_id = data.user_id;
            this.user = data.user ? new User(data.user) : null;
            this.success_chance = data.success_chance;
        }

        /**
         * 检查槽位是否为空
         */
        isEmpty() {
            return this.user_id === null;
        }

        /**
         * 检查是否需要工具
         */
        requiresTool() {
            return this.item_requirement !== null;
        }
    }

    // 定义犯罪任务信息
    class Crime {
        constructor(data) {
            Object.assign(this, {
                id: data.id,
                name: data.name,
                difficulty: data.difficulty,
                status: data.status,
                created_at: data.created_at,
                initiated_at: data.initiated_at,
                ready_at: data.ready_at,
                expired_at: data.expired_at,
                slots: data.slots.map(slot => new Slot(slot)),
                rewards: data.rewards,
                element: null
            });
        }

        setElement(element) {
            this.element = element;
        }

        getSoltNum() {
            return this.slots.length;
        }

        getEmptycNum() {
            return this.slots.reduce((count, slot) => 
                count + (slot.user_id === null ? 1 : 0), 0);
        }

        getCurrentExtraTime() {
            if (this.ready_at === null) return 0;
            return this.ready_at - Utils.getNow();
        }

        getRunTime() {
            return Utils.getNow() - this.initiated_at;
        }

        // 判断crime是否缺人
        isMissingUser() {
            if (this.ready_at === null) return false;
            if (this.getCurrentExtraTime()/3600 <= CONFIG.TIME.URGENT_THRESHOLD && !this.isFullyStaffed()) {
                return true;
            }
            return false;
        }
        // 判断任务是否有人
        isUserd() {
            if (this.getEmptycNum() !== this.getSoltNum()) {
                return true;
            }
            return false;
        }

        // 判断任务是否满人
        isFullyStaffed() {
            if (this.getEmptycNum() == 0) {
                return true;
            }
            return false;
        }

        // 获取DOM信息  
        static getDOMInfo(element) {
            return {
                totalSlots: element.querySelectorAll(CONFIG.UI.SELECTORS.SLOTS).length,
                emptySlots: element.querySelectorAll(CONFIG.UI.SELECTORS.WAITING).length,
                timeElement: element.querySelector(CONFIG.UI.SELECTORS.TITLE)
            };
        }

        static calculateReadyAtTime(element) {
            const { timeElement, emptySlots } = this.getDOMInfo(element);
            const completionTimeStr = timeElement?.textContent?.trim();
            const completionTime = this.EstimateCompletionTime(completionTimeStr);
            return completionTime - emptySlots * CONFIG.TIME.SECONDS_PER_DAY;
        }

        static EstimateCompletionTime(timeStr) {
            if (!timeStr) return null;
            
            try {
                const [days, hours, minutes, seconds] = timeStr.split(':').map(Number);
                return Utils.getNow() + Utils.calculateTimeFromParts(days, hours, minutes, seconds);
            } catch (error) {
                console.error("计算完成时间失败:", error, timeStr);
                return null;
            }
        }
    }

    // =============== UI管理类 ===============
    class CrimeUIManager {
        /**
         * 更新所有犯罪任务的UI
         * @param {HTMLElement} crimeListContainer - 犯罪任务列表容器
         */
        static updateAllCrimesUI(crimeListContainer) {
            if (!crimeListContainer) return;

            // 获取所有crime元素并更新UI
            Array.from(crimeListContainer.children).forEach(element => {
                this.updateSingleCrimeUI(element);
            });
        }

        /**
         * 更新单个犯罪任务的UI
         * @param {HTMLElement} element - 犯罪任务元素
         */
        static updateSingleCrimeUI(element) {
            const crimeNameEl = element.querySelector(CONFIG.UI.SELECTORS.PANEL_TITLE);
            if (!crimeNameEl) return;

            // 获取DOM信息
            const { totalSlots, emptySlots } = Crime.getDOMInfo(element);
            const currentUsers = totalSlots - emptySlots;

            // 计算剩余时间
            const readyAt = Crime.calculateReadyAtTime(element);
            const now = Utils.getNow();
            const extraTimeHours = readyAt ? (readyAt - now) / 3600 : 0;

            // 清除旧的UI
            this.clearUI(element, crimeNameEl);
            
            // 添加新的状态信息
            if (currentUsers > 0) {
            this.addStatusInfo(element, crimeNameEl, {
                currentUsers,
                totalSlots,
                extraTimeHours,
                isFullyStaffed: emptySlots === 0
            });
        }
    }
        /**
         * 清除UI样式
         */
        static clearUI(element, crimeNameEl) {
            element.style.color = '';
            element.style.border = '';
            crimeNameEl.querySelectorAll('span[data-oc-ui]').forEach(span => span.remove());
        }

        /**
         * 添加状态信息
         */
        static addStatusInfo(element, crimeNameEl, stats) {
            const { currentUsers, totalSlots, extraTimeHours, isFullyStaffed } = stats;

            const statusSpan = document.createElement('span');
            statusSpan.setAttribute('data-oc-ui', 'status');
            statusSpan.textContent = `当前${currentUsers}人,共需${totalSlots}人。`;

            this.applyStatusStyle(element, statusSpan, extraTimeHours, isFullyStaffed);
            
            crimeNameEl.appendChild(document.createTextNode(' '));
            crimeNameEl.appendChild(statusSpan);
        }

        /**
         * 应用状态样式
         */
        static applyStatusStyle(element, statusSpan, extraTimeHours, isFullyStaffed) {
            // 基础样式
            statusSpan.style.padding = '4px 8px';
            statusSpan.style.borderRadius = '4px';
            statusSpan.style.fontWeight = '500';
            statusSpan.style.display = 'inline-block';
            statusSpan.style.boxShadow = '0 1px 2px rgba(0,0,0,0.1)';
            statusSpan.style.transition = 'all 0.2s ease';
            statusSpan.style.letterSpacing = '0.3px';

            // 检查是否为移动端
            const isMobile = document.querySelector('.user-information-mobile___WjXnd') !== null;
            statusSpan.style.fontSize = isMobile ? '10px' : '12px';

            if (extraTimeHours <= CONFIG.TIME.URGENT_THRESHOLD && !isFullyStaffed) {
                // 紧急状态
                element.style.border = CONFIG.UI.STYLES.URGENT.BORDER;
                statusSpan.style.background = 'linear-gradient(135deg, #ff4d4d 0%, #e60000 100%)';
                statusSpan.style.color = '#fff';
                statusSpan.style.border = '1px solid #cc0000';
                statusSpan.style.boxShadow = '0 1px 3px rgba(255,0,0,0.2)';
                
                const hours = Math.floor(extraTimeHours);
                const minutes = Math.floor((extraTimeHours % 1) * 60);
                statusSpan.innerHTML = isMobile 
                    ? `<span style="font-size:11px">⚠</span> ${hours}h${minutes}m`
                    : `<span style="font-size:14px;margin-right:4px">⚠</span>急需人手!还剩<strong style="font-weight:600">${hours}小时${minutes}分</strong>`;

            } else if (extraTimeHours <= CONFIG.TIME.STABLE_THRESHOLD) {
                // 稳定状态
                element.style.border = CONFIG.UI.STYLES.STABLE.BORDER;
                statusSpan.style.background = 'linear-gradient(135deg, #4CAF50 0%, #45a049 100%)';
                statusSpan.style.color = '#fff';
                statusSpan.style.border = '1px solid #3d8b40';
                statusSpan.style.boxShadow = '0 1px 3px rgba(0,255,0,0.1)';
                
                statusSpan.innerHTML = isMobile
                    ? `<span style="font-size:11px">✓</span> 配置正常`
                    : `<span style="font-size:14px;margin-right:4px">✓</span>人员配置合理`;

            } else {
                const extraUsers = Math.floor(extraTimeHours/24) - 1;
                if (extraUsers > 0) {
                    // 人员过剩状态
                    element.style.border = CONFIG.UI.STYLES.EXCESS.BORDER;
                    statusSpan.style.background = 'linear-gradient(135deg, #2196F3 0%, #1976D2 100%)';
                    statusSpan.style.color = '#fff';
                    statusSpan.style.border = '1px solid #1565C0';
                    statusSpan.style.boxShadow = '0 1px 3px rgba(0,0,255,0.1)';
                    
                    statusSpan.innerHTML = isMobile
                        ? `<span style="font-size:11px">ℹ</span> 可调${extraUsers}人`
                        : `<span style="font-size:14px;margin-right:4px">ℹ</span>可调配 <strong style="font-weight:600">${extraUsers}</strong> 人至其他OC`;
                } else {
                    // 稳定状态
                    element.style.border = CONFIG.UI.STYLES.STABLE.BORDER;
                    statusSpan.style.background = 'linear-gradient(135deg, #4CAF50 0%, #45a049 100%)';
                    statusSpan.style.color = '#fff';
                    statusSpan.style.border = '1px solid #3d8b40';
                    statusSpan.style.boxShadow = '0 1px 3px rgba(0,255,0,0.1)';
                    
                    statusSpan.innerHTML = isMobile
                        ? `<span style="font-size:11px">✓</span> 配置正常`
                        : `<span style="font-size:14px;margin-right:4px">✓</span>人员配置合理`;
                }
            }

            // 添加悬停效果
            statusSpan.addEventListener('mouseover', () => {
                statusSpan.style.transform = 'translateY(-1px)';
                statusSpan.style.boxShadow = statusSpan.style.boxShadow.replace('3px', '4px');
            });

            statusSpan.addEventListener('mouseout', () => {
                statusSpan.style.transform = 'translateY(0)';
                statusSpan.style.boxShadow = statusSpan.style.boxShadow.replace('4px', '3px');
            });
        }
    }

    // =============== API管理类 ===============
    class APIManager {
        /**
         * 从API获取最新的犯罪数据
         * @returns {Promise<Object>} 犯罪数据
         */
        static async fetchCrimeData() {
            try {
                const response = await fetch(
                    `${CONFIG.API.URL}?key=${CONFIG.API.KEY}&cat=${CONFIG.API.PARAMS.CATEGORY}`
                );
                const data = await response.json();

                if (data.error) {
                    throw new Error(`API错误: ${data.error.error}`);
                }

                return {
                    crimes: data.crimes.map(crime => new Crime(crime)),
                    _metadata: data._metadata,
                    timestamp: Date.now()
                };
            } catch (error) {
                console.error('获取犯罪数据失败:', error);
                throw error;
            }
        }

        /**
         * 获取犯罪数据(优先使用缓存)
         * @returns {Promise<Object>} 犯罪数据
         */
        static async getCrimeData() {
            // 先尝试获取缓存的数据
            const cachedData = CacheManager.getCachedData();
            if (cachedData) {
                console.log('使用缓存的犯罪数据');
                return cachedData;  // 已经在getCachedData中重建了Crime对象
            }

            // 如果没有缓存或缓存过期,从API获取新数据
            console.log('从API获取新的犯罪数据');
            const newData = await this.fetchCrimeData();
            CacheManager.cacheData(newData);
            return newData;
        }

        /**
         * 从Torn API获取玩家基本信息
         * @returns {Promise<Object>} 玩家信息
         */
        static async fetchPlayerInfo() {
            try {
                const response = await fetch(`https://api.torn.com/user/?selections=basic&key=${CONFIG.API.KEY}`);
                const data = await response.json();
                if (data.error) {
                    throw new Error(`API错误: ${data.error.error}`);
                }

                return data;
            } catch (error) {
                console.error('获取玩家信息失败:', error);
                throw error;
            }
        }
    }

    // =============== 状态图标管理类 ===============
    class StatusIconManager {
        constructor(crimeInfo) {
            this.crimeInfo = crimeInfo;
        }

        /**
         * 检查是否为移动端
         * @returns {boolean}
         */
        isMobileDevice() {
            return document.querySelector('.user-information-mobile___WjXnd') !== null;
        }

        /**
         * 获取状态容器父元素
         * @returns {HTMLElement|null}
         */
        getStatusContainerParent() {
            if (this.isMobileDevice()) {
                return document.querySelector('.user-information-mobile___WjXnd');
            } else {
                return document.getElementsByClassName("status-icons___gPkXF")[0]?.parentNode;
            }
        }

        /**
         * 更新状态图标
         */
        updateStatusIcons(userId) {
            const containerParent = this.getStatusContainerParent();
            if (!containerParent) return;

            const ocStatusContainer = this.createStatusContainer();
            this.removeOldContainer();

            const userCrime = this.findUserCrime(userId);
            if (userCrime) {
                this.renderParticipatingStatus(ocStatusContainer, userCrime, userId);
            } else {
                this.renderNonParticipatingStatus(ocStatusContainer, userId);
            }

            // 移动端时添加额外的样式
            if (this.isMobileDevice()) {
                ocStatusContainer.style.margin = '10px 15px';
                ocStatusContainer.style.width = 'calc(100% - 30px)'; // 考虑左右margin
            }

            containerParent.appendChild(ocStatusContainer);
        }

        /**
         * 查找用户参与的犯罪任务
         */
        findUserCrime(userId) {
            return this.crimeInfo.crimes.find(crime => 
                crime.slots.some(slot => slot.user_id === userId)
            );
        }

        /**
         * 创建状态容器
         */
        createStatusContainer() {
            const container = document.createElement('div');
            container.style.display = 'flex';
            container.style.flexDirection = 'column';
            container.style.marginTop = '10px';
            container.id = 'oc-status-container';
            return container;
        }

        /**
         * 移除旧的状态容器
         */
        removeOldContainer() {
            const oldContainer = document.getElementById('oc-status-container');
            if (oldContainer) {
                oldContainer.remove();
            }
        }

        /**
         * 渲染参与中的状态
         */
        renderParticipatingStatus(container, userCrime, userId) {
            const slotIcons = this.createSlotIconsContainer();
            
            // 添加点击事件,跳转到对应的OC任务
            slotIcons.style.cursor = 'pointer';
            slotIcons.addEventListener('click', () => {
                window.location.href = `https://www.torn.com/factions.php?step=your#/tab=crimes&crimeId=${userCrime.id}`;
            });

            userCrime.slots.forEach(slot => {
                const icon = this.createSlotIcon(slot, userId);
                slotIcons.appendChild(icon);
            });
            container.appendChild(slotIcons);
        }

        /**
         * 渲染未参与的状态
         */
        renderNonParticipatingStatus(container, userId) {
            const notInOCContainer = this.createNotInOCContainer();
            const textSpan = this.createTextSpan();
            const targetCrime = this.findBestAvailableCrime();
            const joinLink = this.createJoinLink(targetCrime?.id || '', userId);

            notInOCContainer.appendChild(textSpan);
            notInOCContainer.appendChild(joinLink);
            container.appendChild(notInOCContainer);
        }

        /**
         * 创建slot图标容器
         */
        createSlotIconsContainer() {
            const container = document.createElement('div');
            container.style.display = 'flex';
            container.style.alignItems = 'center';
            container.style.height = '17px';
            container.style.cursor = 'pointer';
            
            // 添加渐变背景和质感效果
            container.style.background = 'linear-gradient(to bottom, rgba(30,30,30,0.02) 0%, rgba(0,0,0,0.02) 100%)';
            container.style.border = '1px solid rgba(128, 128, 128, 0.2)';
            container.style.borderRadius = '3px';
            container.style.padding = '3px 5px 3px 0px';
            container.style.boxShadow = 'inset 0 1px 0 rgba(255,255,255,0.05), 0 1px 2px rgba(0,0,0,0.02)';
            
            // 添加悬停效果
            container.addEventListener('mouseover', () => {
                container.style.background = 'linear-gradient(to bottom, rgba(30,30,30,0.04) 0%, rgba(0,0,0,0.04) 100%)';
                container.style.boxShadow = 'inset 0 1px 0 rgba(255,255,255,0.08), 0 1px 3px rgba(0,0,0,0.03)';
                container.style.transition = 'all 0.2s ease';
            });

            container.addEventListener('mouseout', () => {
                container.style.background = 'linear-gradient(to bottom, rgba(30,30,30,0.02) 0%, rgba(0,0,0,0.02) 100%)';
                container.style.boxShadow = 'inset 0 1px 0 rgba(255,255,255,0.05), 0 1px 2px rgba(0,0,0,0.02)';
            });

            return container;
        }

        /**
         * 创建slot图标
         */
        createSlotIcon(slot, userId) {
            const icon = document.createElement('div');
            icon.style.width = '17px';
            icon.style.height = '17px';
            icon.style.borderRadius = '50%';
            icon.style.margin = '5px 10px 5px 0px';
            icon.style.boxSizing = 'border-box';
            icon.style.display = 'flex';
            icon.style.alignItems = 'center';
            icon.style.justifyContent = 'center';
            icon.style.position = 'relative';
            
            // 添加渐变和阴影效果
            if (slot.user) {
                // 已加入状态 - 绿色渐变
                icon.style.background = 'linear-gradient(135deg, #5cb85c 0%, #4CAF50 100%)';
                icon.style.border = '1px solid #45a049';
                icon.style.boxShadow = 'inset 0 1px 1px rgba(255,255,255,0.2), 0 1px 2px rgba(0,0,0,0.1)';
            } else {
                // 空位状态 - 灰色渐变
                icon.style.background = 'linear-gradient(135deg, #a4a4a4 0%, #9E9E9E 100%)';
                icon.style.border = '1px solid #888';
                icon.style.boxShadow = 'inset 0 1px 1px rgba(255,255,255,0.1), 0 1px 2px rgba(0,0,0,0.1)';
            }

            let progressInfo = '';
            if (slot.user) {
                progressInfo = `${slot.user_id} 在这`;
                if (slot.user_id === userId) {
                    this.addPlayerMarker(icon);
                }
            } else {
                progressInfo = '空位';
            }

            if (slot.item_requirement) {
                this.addToolMark(icon);
                progressInfo += '\n需要工具';
            }

            icon.title = progressInfo;

            // 添加悬停效果
            icon.addEventListener('mouseover', () => {
                icon.style.transform = 'scale(1.1)';
                icon.style.transition = 'all 0.2s ease';
                icon.style.boxShadow = slot.user 
                    ? 'inset 0 1px 2px rgba(255,255,255,0.3), 0 2px 4px rgba(0,0,0,0.2)'
                    : 'inset 0 1px 2px rgba(255,255,255,0.2), 0 2px 4px rgba(0,0,0,0.2)';
            });

            icon.addEventListener('mouseout', () => {
                icon.style.transform = 'scale(1)';
                icon.style.boxShadow = slot.user
                    ? 'inset 0 1px 1px rgba(255,255,255,0.2), 0 1px 2px rgba(0,0,0,0.1)'
                    : 'inset 0 1px 1px rgba(255,255,255,0.1), 0 1px 2px rgba(0,0,0,0.1)';
            });

            // 创建自定义tooltip
            const tooltip = document.createElement('div');
            tooltip.style.position = 'absolute';
            tooltip.style.visibility = 'hidden';
            tooltip.style.backgroundColor = 'rgba(40, 40, 40, 0.95)';
            tooltip.style.color = '#fff';
            tooltip.style.padding = '8px 12px';
            tooltip.style.borderRadius = '4px';
            tooltip.style.fontSize = '12px';
            tooltip.style.lineHeight = '1.4';
            tooltip.style.whiteSpace = 'nowrap';
            tooltip.style.zIndex = '1000';
            tooltip.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
            tooltip.style.transform = 'translateY(-5px)';
            tooltip.style.transition = 'all 0.2s ease';

            // 设置tooltip内容
            let tooltipContent = slot.user 
                ? `<div style="font-weight:500">${slot.user_id} 在这</div>`
                : '<div style="color:#aaa">空位</div>';
            
            if (slot.item_requirement) {
                tooltipContent += `
                    <div style="margin-top:4px;padding-top:4px;border-top:1px solid rgba(255,255,255,0.1)">
                        <span style="color:#FFA000">⚠</span> 需要工具
                    </div>`;
            }
            tooltip.innerHTML = tooltipContent;

            // 添加tooltip显示/隐藏逻辑
            icon.addEventListener('mouseenter', (e) => {
                tooltip.style.visibility = 'visible';
                tooltip.style.opacity = '1';
                tooltip.style.transform = 'translateY(0)';
                
                // 计算位置
                const rect = icon.getBoundingClientRect();
                tooltip.style.left = rect.left + 'px';
                tooltip.style.top = (rect.top - tooltip.offsetHeight - 10) + 'px';
            });

            icon.addEventListener('mouseleave', () => {
                tooltip.style.visibility = 'hidden';
                tooltip.style.opacity = '0';
                tooltip.style.transform = 'translateY(-5px)';
            });

            document.body.appendChild(tooltip);
            icon.title = ''; // 移除默认tooltip
            return icon;
        }

        /**
         * 添加玩家标记
         */
        addPlayerMarker(icon) {
            const marker = document.createElement('span');
            marker.innerHTML = '★';
            marker.style.color = 'white';
            marker.style.fontSize = '10px';
            marker.style.textShadow = '0 0 1px #000';
            icon.appendChild(marker);
        }

        /**
         * 添加工具标记
         */
        addToolMark(icon) {
            const toolMark = document.createElement('div');
            toolMark.style.position = 'absolute';
            toolMark.style.bottom = '0';
            toolMark.style.right = '0';
            toolMark.style.width = '6px';
            toolMark.style.height = '6px';
            toolMark.style.backgroundColor = '#FFC107';
            toolMark.style.borderRadius = '50%';
            toolMark.style.border = '1px solid #FFA000';
            toolMark.style.transform = 'translate(25%, 25%)';
            icon.appendChild(toolMark);
        }

        /**
         * 创建未参加OC的容器
         */
        createNotInOCContainer() {
            const container = document.createElement('div');
            container.style.display = 'flex';
            container.style.alignItems = 'center';
            container.style.gap = '5px';
            container.style.backgroundColor = '#F44336';
            container.style.padding = '3px 8px';
            container.style.borderRadius = '3px';
            container.style.marginBottom = '10px';
            return container;
        }

        /**
         * 创建提示文本
         */
        createTextSpan() {
            const textSpan = document.createElement('span');
            textSpan.textContent = '未加入oc,';
            textSpan.style.fontSize = '12px';
            textSpan.style.color = 'white';
            return textSpan;
        }

        /**
         * 查找最佳可用的犯罪任务
         */
        findBestAvailableCrime() {
            let targetCrime = this.crimeInfo.crimes.find(crime => 
                crime.isMissingUser()
            );

            if (!targetCrime) {
                const emptyCrimes = this.crimeInfo.crimes.filter(crime => 
                    !crime.isUserd()
                );

                if (emptyCrimes.length > 0) {
                    targetCrime = emptyCrimes.reduce((highest, current) => 
                        current.difficulty > highest.difficulty ? current : highest
                    );
                } else {
                    const availableCrimes = this.crimeInfo.crimes.filter(crime => 
                        crime.slots.some(slot => !slot.user)
                    );
                    targetCrime = availableCrimes.reduce((highest, current) => 
                        current.difficulty > highest.difficulty ? current : highest
                    );
                }
            }

            return targetCrime;
        }

        /**
         * 创建加入链接
         */
        createJoinLink(crimeId, userId) {
            const joinLink = document.createElement('a');
            joinLink.textContent = 'join';
            joinLink.href = `https://www.torn.com/factions.php?step=your#/tab=crimes&crimeId=${crimeId}`;
            joinLink.style.color = 'white';
            joinLink.style.textDecoration = 'underline';
            joinLink.style.fontSize = '13px';
            joinLink.style.fontWeight = 'bold';
            joinLink.style.textShadow = '0 0 1px rgba(255, 255, 255, 0.5)';
            joinLink.style.letterSpacing = '0.5px';

            this.addJoinLinkEffects(joinLink, userId);
            return joinLink;
        }

        /**
         * 添加加入链接效果
         */
        addJoinLinkEffects(joinLink, userId) {
            joinLink.addEventListener('mouseover', () => {
                joinLink.style.textShadow = '0 0 2px rgba(255, 255, 255, 0.8)';
                joinLink.style.transition = 'all 0.2s ease';
            });

            joinLink.addEventListener('mouseout', () => {
                joinLink.style.textShadow = '0 0 1px rgba(255, 255, 255, 0.5)';
            });

            joinLink.addEventListener('click', async () => {
                try {
                    const newData = await APIManager.fetchCrimeData();
                    this.crimeInfo = newData;
                    const playerInfo = await APIManager.fetchPlayerInfo();
                    this.updateStatusIcons(playerInfo.player_id);
                } catch (error) {
                    console.error('更新OC数据失败:', error);
                }
            });
        }
    }

    // =============== 主程序类 ===============
    class OCFacilitation {
        constructor() {
            this.crimeInfo = null;
            this.currentTab = null;
            this.isInitialized = false;
            this.isUpdating = false;
            this.observer = null;
            this.statusIconManager = null;
        }

        /**
         * 处理页面变化
         */
        async handlePageChange() {
            if (!Utils.isFactionPage() || !Utils.isOCPage()) {
                this.cleanup();
                return;
            }

            try {
                if (!this.crimeInfo) {
                    this.crimeInfo = await APIManager.fetchCrimeData();
                }
                
                const container = await Utils.waitForWrapper();
                await this.handleInitialUIUpdate(container);
                
                // 如果还没有设置观察器,设置它
                if (!this.observer) {
                    this.setupObserver(container);
                }
            } catch (error) {
                console.error('处理页面变化失败:', error);
            }
        }

        /**
         * 处理初始UI更新
         * @param {HTMLElement} container - 犯罪任务列表容器
         */
        async handleInitialUIUpdate(container) {
            await new Promise(resolve => setTimeout(resolve, CONFIG.UI.LOAD_DELAY));
            await this.updateCrimeListUI(container);
        }

        /**
         * 更新犯罪任务列表UI
         * @param {HTMLElement} container - 犯罪任务列表容器
         */
        async updateCrimeListUI(container) {
            if (this.isUpdating) return;

            try {
                this.isUpdating = true;
                CrimeUIManager.updateAllCrimesUI(container);
            } finally {
                this.isUpdating = false;
            }
        }

        /**
         * 设置观察器
         * @param {HTMLElement} container - 犯罪任务列表容器
         */
        setupObserver(container) {
            this.observer = new MutationObserver(Utils.debounce((mutations) => {
                const hasChildrenChanges = mutations.some(mutation => 
                    mutation.type === 'childList' && 
                    mutation.target === container
                );

                if (hasChildrenChanges) {
                    this.updateCrimeListUI(container)
                        .catch(error => console.error('更新犯罪任务UI失败:', error));
                }
            }, CONFIG.UI.UPDATE_DEBOUNCE));

            this.observer.observe(container, {
                childList: true,
                subtree: false,
                attributes: false,
                characterData: false
            });
        }

        /**
         * 清理资源
         */
        cleanup() {
            if (this.observer) {
                this.observer.disconnect();
                this.observer = null;
            }
            this.isUpdating = false;
        }

        /**
         * 初始化程序
         */
        async initialize() {
            try {
                await this.initializeData();
                await this.setupStatusIcons();
                await this.setupPageChangeListeners();
                
                this.isInitialized = true;
            } catch (error) {
                console.error('初始化失败:', error);
            }
        }

        /**
         * 初始化数据
         */
        async initializeData() {
            // 直接从API获取新数据
            this.crimeInfo = await APIManager.fetchCrimeData();
            this.statusIconManager = new StatusIconManager(this.crimeInfo);
        }

        /**
         * 设置UI
         */
        async setupStatusIcons() {
            // 获取玩家信息并更新状态图标
            const playerInfo = await APIManager.fetchPlayerInfo();
            this.statusIconManager.updateStatusIcons(playerInfo.player_id);
        }

        /**
         * 设置页面变化监听器
         */
        setupPageChangeListeners() {
            // 监听hash变化(页签切换)
            window.addEventListener('hashchange', () => this.handlePageChange());
            
            // 监听页面加载完成
            if (document.readyState === 'complete') {
                this.handlePageChange();
            } else {
                window.addEventListener('load', () => this.handlePageChange());
            }
        }
    }

    // 启动程序
    (() => {
        const app = new OCFacilitation();
        app.initialize();
        
        // 页面卸载时清理资源
        window.addEventListener('unload', () => {
            app.cleanup();
        });
    })();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址