OCFacilitation

Make OC 2.0 easier for regular players

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         OCFacilitation
// @name:zh-CN   OC协助工具
// @namespace    https://greasyfork.org/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();
        });
    })();
})();