// ==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
// @downloadURL https://update.greasyfork.cloud/scripts/523280/OCFacilitation.user.js
// @updateURL https://update.greasyfork.cloud/scripts/523280/OCFacilitation.meta.js
// ==/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
? `⚠ ${hours}h${minutes}m`
: `⚠急需人手!还剩${hours}小时${minutes}分`;
} 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
? `✓ 配置正常`
: `✓人员配置合理`;
} 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
? `ℹ 可调${extraUsers}人`
: `ℹ可调配 ${extraUsers} 人至其他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
? `✓ 配置正常`
: `✓人员配置合理`;
}
}
// 添加悬停效果
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