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