OCFacilitation

Make OC 2.0 easier for regular players

  1. // ==UserScript==
  2. // @name OCFacilitation
  3. // @name:zh-CN OC协助工具
  4. // @namespace https://gf.qytechs.cn/users/[daluo]
  5. // @version 1.0.2.3
  6. // @description Make OC 2.0 easier for regular players
  7. // @description:zh-CN 使普通玩家oc2.0更简单和方便
  8. // @author daluo
  9. // @match https://www.torn.com/*
  10. // @license MIT
  11. // @grant none
  12. // ==/UserScript==
  13. (function() {
  14. 'use strict';
  15. const APIKey = "不使用冰蛙的大佬,替换成自己的apiKey,limit就可以";
  16. // =============== 配置管理 ===============
  17. const CONFIG = {
  18. API: {
  19. KEY: (() => {
  20. try {
  21. // 尝试多种方式获取API Key
  22. return localStorage.getItem("APIKey") ||
  23. GM_getValue("APIKey")
  24. } catch (error) {
  25. console.error('获取API Key失败:', error);
  26. return APIKey
  27. }
  28. })(),
  29. URL: 'https://api.torn.com/v2/faction/crimes',
  30. PARAMS: { CATEGORY: 'available' }
  31. },
  32. UI: {
  33. LOAD_DELAY: 300,
  34. UPDATE_DEBOUNCE: 500,
  35. TIME_TOLERANCE: 2,
  36. SELECTORS: {
  37. WRAPPER: '.wrapper___U2Ap7',
  38. SLOTS: '.wrapper___Lpz_D',
  39. WAITING: '.waitingJoin___jq10k',
  40. TITLE: '.title___pB5FU',
  41. PANEL_TITLE: '.panelTitle___aoGuV',
  42. MOBILE_INFO: '.user-information-mobile___WjXnd'
  43. },
  44. STYLES: {
  45. URGENT: {
  46. BORDER: '3px solid red',
  47. COLOR: 'red'
  48. },
  49. STABLE: {
  50. BORDER: '3px solid green',
  51. COLOR: 'green'
  52. },
  53. EXCESS: {
  54. BORDER: '3px solid yellow',
  55. COLOR: 'blue'
  56. }
  57. }
  58. },
  59. TIME: {
  60. SECONDS_PER_DAY: 86400,
  61. HOURS_PER_DAY: 24,
  62. URGENT_THRESHOLD: 12,
  63. STABLE_THRESHOLD: 36
  64. }
  65. };
  66.  
  67. // =============== 工具类 ===============
  68. class Utils {
  69. /**
  70. * 获取当前页签名称
  71. * @returns {string|null} 页签名称
  72. */
  73. static getCurrentTab() {
  74. const match = window.location.hash.match(/#\/tab=([^&]*)/);
  75. return match ? match[1] : null;
  76. }
  77.  
  78. /**
  79. * 检查当前页面是否为OC页面
  80. * @returns {boolean}
  81. */
  82. static isOCPage() {
  83. return this.getCurrentTab() === 'crimes';
  84. }
  85.  
  86. /**
  87. * 检查是否为移动端
  88. * @returns {boolean}
  89. */
  90. static isMobileDevice() {
  91. return document.querySelector(CONFIG.UI.SELECTORS.MOBILE_INFO) !== null;
  92. }
  93.  
  94. /**
  95. * 获取当前时间戳(秒)
  96. * @returns {number}
  97. */
  98. static getNow() {
  99. return Math.floor(Date.now() / 1000);
  100. }
  101.  
  102. /**
  103. * 防抖函数
  104. * @param {Function} func - 需要防抖的函数
  105. * @param {number} wait - 等待时间(毫秒)
  106. */
  107. static debounce(func, wait) {
  108. let timeout;
  109. return function executedFunction(...args) {
  110. clearTimeout(timeout);
  111. timeout = setTimeout(() => func.apply(this, args), wait);
  112. };
  113. }
  114.  
  115. /**
  116. * 检查URL是否包含factions.php
  117. * @returns {boolean} 是否为faction页面
  118. */
  119. static isFactionPage() {
  120. return window.location.pathname === '/factions.php';
  121. }
  122.  
  123. static waitForElement(selector) {
  124. return new Promise(resolve => {
  125. const element = document.querySelector(selector);
  126. if (element) return resolve(element);
  127.  
  128. const observer = new MutationObserver(mutations => {
  129. const element = document.querySelector(selector);
  130. if (element) {
  131. observer.disconnect();
  132. resolve(element);
  133. }
  134. });
  135.  
  136. observer.observe(document.body, {
  137. childList: true,
  138. subtree: true
  139. });
  140. });
  141. }
  142.  
  143. static calculateTimeFromParts(days, hours, minutes, seconds) {
  144. return (days * CONFIG.TIME.SECONDS_PER_DAY) +
  145. (hours * 3600) +
  146. (minutes * 60) +
  147. seconds;
  148. }
  149.  
  150. static async waitForWrapper() {
  151. const maxAttempts = 10;
  152. const interval = 1000; // 1秒
  153.  
  154. for (let attempts = 0; attempts < maxAttempts; attempts++) {
  155. const wrapper = document.querySelector(CONFIG.UI.SELECTORS.WRAPPER);
  156. if (wrapper?.parentNode) {
  157. return wrapper.parentNode;
  158. }
  159. await Utils.delay(interval);
  160. }
  161. throw new Error('无法找到wrapper元素');
  162. }
  163.  
  164. static delay(ms) {
  165. return new Promise(resolve => setTimeout(resolve, ms));
  166. }
  167. }
  168.  
  169. // =============== 数据模型 ===============
  170. /**
  171. * 任务物品需求类
  172. */
  173. class ItemRequirement {
  174. constructor(data) {
  175. this.id = data.id;
  176. this.is_reusable = data.is_reusable;
  177. this.is_available = data.is_available;
  178. }
  179. }
  180.  
  181. /**
  182. * 用户信息类
  183. */
  184. class User {
  185. constructor(data) {
  186. if (!data) return null;
  187. this.id = data.id;
  188. this.joined_at = data.joined_at;
  189. }
  190. }
  191.  
  192. /**
  193. * 任务槽位类
  194. */
  195. class Slot {
  196. constructor(data) {
  197. this.position = data.position;
  198. this.item_requirement = data.item_requirement ? new ItemRequirement(data.item_requirement) : null;
  199. this.user_id = data.user_id;
  200. this.user = data.user ? new User(data.user) : null;
  201. this.success_chance = data.success_chance;
  202. }
  203.  
  204. /**
  205. * 检查槽位是否为空
  206. */
  207. isEmpty() {
  208. return this.user_id === null;
  209. }
  210.  
  211. /**
  212. * 检查是否需要工具
  213. */
  214. requiresTool() {
  215. return this.item_requirement !== null;
  216. }
  217. }
  218.  
  219. // 定义犯罪任务信息
  220. class Crime {
  221. constructor(data) {
  222. Object.assign(this, {
  223. id: data.id,
  224. name: data.name,
  225. difficulty: data.difficulty,
  226. status: data.status,
  227. created_at: data.created_at,
  228. initiated_at: data.initiated_at,
  229. ready_at: data.ready_at,
  230. expired_at: data.expired_at,
  231. slots: data.slots.map(slot => new Slot(slot)),
  232. rewards: data.rewards,
  233. element: null
  234. });
  235. }
  236.  
  237. setElement(element) {
  238. this.element = element;
  239. }
  240.  
  241. getSoltNum() {
  242. return this.slots.length;
  243. }
  244.  
  245. getEmptycNum() {
  246. return this.slots.reduce((count, slot) =>
  247. count + (slot.user_id === null ? 1 : 0), 0);
  248. }
  249.  
  250. getCurrentExtraTime() {
  251. if (this.ready_at === null) return 0;
  252. return this.ready_at - Utils.getNow();
  253. }
  254.  
  255. getRunTime() {
  256. return Utils.getNow() - this.initiated_at;
  257. }
  258.  
  259. // 判断crime是否缺人
  260. isMissingUser() {
  261. if (this.ready_at === null) return false;
  262. if (this.getCurrentExtraTime()/3600 <= CONFIG.TIME.URGENT_THRESHOLD && !this.isFullyStaffed()) {
  263. return true;
  264. }
  265. return false;
  266. }
  267. // 判断任务是否有人
  268. isUserd() {
  269. if (this.getEmptycNum() !== this.getSoltNum()) {
  270. return true;
  271. }
  272. return false;
  273. }
  274.  
  275. // 判断任务是否满人
  276. isFullyStaffed() {
  277. if (this.getEmptycNum() == 0) {
  278. return true;
  279. }
  280. return false;
  281. }
  282.  
  283. // 获取DOM信息
  284. static getDOMInfo(element) {
  285. return {
  286. totalSlots: element.querySelectorAll(CONFIG.UI.SELECTORS.SLOTS).length,
  287. emptySlots: element.querySelectorAll(CONFIG.UI.SELECTORS.WAITING).length,
  288. timeElement: element.querySelector(CONFIG.UI.SELECTORS.TITLE)
  289. };
  290. }
  291.  
  292. static calculateReadyAtTime(element) {
  293. const { timeElement, emptySlots } = this.getDOMInfo(element);
  294. const completionTimeStr = timeElement?.textContent?.trim();
  295. const completionTime = this.EstimateCompletionTime(completionTimeStr);
  296. return completionTime - emptySlots * CONFIG.TIME.SECONDS_PER_DAY;
  297. }
  298.  
  299. static EstimateCompletionTime(timeStr) {
  300. if (!timeStr) return null;
  301. try {
  302. const [days, hours, minutes, seconds] = timeStr.split(':').map(Number);
  303. return Utils.getNow() + Utils.calculateTimeFromParts(days, hours, minutes, seconds);
  304. } catch (error) {
  305. console.error("计算完成时间失败:", error, timeStr);
  306. return null;
  307. }
  308. }
  309. }
  310.  
  311. // =============== UI管理类 ===============
  312. class CrimeUIManager {
  313. /**
  314. * 更新所有犯罪任务的UI
  315. * @param {HTMLElement} crimeListContainer - 犯罪任务列表容器
  316. */
  317. static updateAllCrimesUI(crimeListContainer) {
  318. if (!crimeListContainer) return;
  319.  
  320. // 获取所有crime元素并更新UI
  321. Array.from(crimeListContainer.children).forEach(element => {
  322. this.updateSingleCrimeUI(element);
  323. });
  324. }
  325.  
  326. /**
  327. * 更新单个犯罪任务的UI
  328. * @param {HTMLElement} element - 犯罪任务元素
  329. */
  330. static updateSingleCrimeUI(element) {
  331. const crimeNameEl = element.querySelector(CONFIG.UI.SELECTORS.PANEL_TITLE);
  332. if (!crimeNameEl) return;
  333.  
  334. // 获取DOM信息
  335. const { totalSlots, emptySlots } = Crime.getDOMInfo(element);
  336. const currentUsers = totalSlots - emptySlots;
  337.  
  338. // 计算剩余时间
  339. const readyAt = Crime.calculateReadyAtTime(element);
  340. const now = Utils.getNow();
  341. const extraTimeHours = readyAt ? (readyAt - now) / 3600 : 0;
  342.  
  343. // 清除旧的UI
  344. this.clearUI(element, crimeNameEl);
  345. // 添加新的状态信息
  346. if (currentUsers > 0) {
  347. this.addStatusInfo(element, crimeNameEl, {
  348. currentUsers,
  349. totalSlots,
  350. extraTimeHours,
  351. isFullyStaffed: emptySlots === 0
  352. });
  353. }
  354. }
  355. /**
  356. * 清除UI样式
  357. */
  358. static clearUI(element, crimeNameEl) {
  359. element.style.color = '';
  360. element.style.border = '';
  361. crimeNameEl.querySelectorAll('span[data-oc-ui]').forEach(span => span.remove());
  362. }
  363.  
  364. /**
  365. * 添加状态信息
  366. */
  367. static addStatusInfo(element, crimeNameEl, stats) {
  368. const { currentUsers, totalSlots, extraTimeHours, isFullyStaffed } = stats;
  369.  
  370. const statusSpan = document.createElement('span');
  371. statusSpan.setAttribute('data-oc-ui', 'status');
  372. statusSpan.textContent = `当前${currentUsers}人,共需${totalSlots}人。`;
  373.  
  374. this.applyStatusStyle(element, statusSpan, extraTimeHours, isFullyStaffed);
  375. crimeNameEl.appendChild(document.createTextNode(' '));
  376. crimeNameEl.appendChild(statusSpan);
  377. }
  378.  
  379. /**
  380. * 应用状态样式
  381. */
  382. static applyStatusStyle(element, statusSpan, extraTimeHours, isFullyStaffed) {
  383. // 基础样式
  384. statusSpan.style.padding = '4px 8px';
  385. statusSpan.style.borderRadius = '4px';
  386. statusSpan.style.fontWeight = '500';
  387. statusSpan.style.display = 'inline-block';
  388. statusSpan.style.boxShadow = '0 1px 2px rgba(0,0,0,0.1)';
  389. statusSpan.style.transition = 'all 0.2s ease';
  390. statusSpan.style.letterSpacing = '0.3px';
  391.  
  392. // 检查是否为移动端
  393. const isMobile = document.querySelector('.user-information-mobile___WjXnd') !== null;
  394. statusSpan.style.fontSize = isMobile ? '10px' : '12px';
  395.  
  396. if (extraTimeHours <= CONFIG.TIME.URGENT_THRESHOLD && !isFullyStaffed) {
  397. // 紧急状态
  398. element.style.border = CONFIG.UI.STYLES.URGENT.BORDER;
  399. statusSpan.style.background = 'linear-gradient(135deg, #ff4d4d 0%, #e60000 100%)';
  400. statusSpan.style.color = '#fff';
  401. statusSpan.style.border = '1px solid #cc0000';
  402. statusSpan.style.boxShadow = '0 1px 3px rgba(255,0,0,0.2)';
  403. const hours = Math.floor(extraTimeHours);
  404. const minutes = Math.floor((extraTimeHours % 1) * 60);
  405. statusSpan.innerHTML = isMobile
  406. ? `<span style="font-size:11px">⚠</span> ${hours}h${minutes}m`
  407. : `<span style="font-size:14px;margin-right:4px">⚠</span>急需人手!还剩<strong style="font-weight:600">${hours}小时${minutes}分</strong>`;
  408.  
  409. } else if (extraTimeHours <= CONFIG.TIME.STABLE_THRESHOLD) {
  410. // 稳定状态
  411. element.style.border = CONFIG.UI.STYLES.STABLE.BORDER;
  412. statusSpan.style.background = 'linear-gradient(135deg, #4CAF50 0%, #45a049 100%)';
  413. statusSpan.style.color = '#fff';
  414. statusSpan.style.border = '1px solid #3d8b40';
  415. statusSpan.style.boxShadow = '0 1px 3px rgba(0,255,0,0.1)';
  416. statusSpan.innerHTML = isMobile
  417. ? `<span style="font-size:11px">✓</span> 配置正常`
  418. : `<span style="font-size:14px;margin-right:4px">✓</span>人员配置合理`;
  419.  
  420. } else {
  421. const extraUsers = Math.floor(extraTimeHours/24) - 1;
  422. if (extraUsers > 0) {
  423. // 人员过剩状态
  424. element.style.border = CONFIG.UI.STYLES.EXCESS.BORDER;
  425. statusSpan.style.background = 'linear-gradient(135deg, #2196F3 0%, #1976D2 100%)';
  426. statusSpan.style.color = '#fff';
  427. statusSpan.style.border = '1px solid #1565C0';
  428. statusSpan.style.boxShadow = '0 1px 3px rgba(0,0,255,0.1)';
  429. statusSpan.innerHTML = isMobile
  430. ? `<span style="font-size:11px">ℹ</span> 可调${extraUsers}人`
  431. : `<span style="font-size:14px;margin-right:4px">ℹ</span>可调配 <strong style="font-weight:600">${extraUsers}</strong> 人至其他OC`;
  432. } else {
  433. // 稳定状态
  434. element.style.border = CONFIG.UI.STYLES.STABLE.BORDER;
  435. statusSpan.style.background = 'linear-gradient(135deg, #4CAF50 0%, #45a049 100%)';
  436. statusSpan.style.color = '#fff';
  437. statusSpan.style.border = '1px solid #3d8b40';
  438. statusSpan.style.boxShadow = '0 1px 3px rgba(0,255,0,0.1)';
  439. statusSpan.innerHTML = isMobile
  440. ? `<span style="font-size:11px">✓</span> 配置正常`
  441. : `<span style="font-size:14px;margin-right:4px">✓</span>人员配置合理`;
  442. }
  443. }
  444.  
  445. // 添加悬停效果
  446. statusSpan.addEventListener('mouseover', () => {
  447. statusSpan.style.transform = 'translateY(-1px)';
  448. statusSpan.style.boxShadow = statusSpan.style.boxShadow.replace('3px', '4px');
  449. });
  450.  
  451. statusSpan.addEventListener('mouseout', () => {
  452. statusSpan.style.transform = 'translateY(0)';
  453. statusSpan.style.boxShadow = statusSpan.style.boxShadow.replace('4px', '3px');
  454. });
  455. }
  456. }
  457.  
  458. // =============== API管理类 ===============
  459. class APIManager {
  460. /**
  461. * 从API获取最新的犯罪数据
  462. * @returns {Promise<Object>} 犯罪数据
  463. */
  464. static async fetchCrimeData() {
  465. try {
  466. const response = await fetch(
  467. `${CONFIG.API.URL}?key=${CONFIG.API.KEY}&cat=${CONFIG.API.PARAMS.CATEGORY}`
  468. );
  469. const data = await response.json();
  470.  
  471. if (data.error) {
  472. throw new Error(`API错误: ${data.error.error}`);
  473. }
  474.  
  475. return {
  476. crimes: data.crimes.map(crime => new Crime(crime)),
  477. _metadata: data._metadata,
  478. timestamp: Date.now()
  479. };
  480. } catch (error) {
  481. console.error('获取犯罪数据失败:', error);
  482. throw error;
  483. }
  484. }
  485.  
  486. /**
  487. * 获取犯罪数据(优先使用缓存)
  488. * @returns {Promise<Object>} 犯罪数据
  489. */
  490. static async getCrimeData() {
  491. // 先尝试获取缓存的数据
  492. const cachedData = CacheManager.getCachedData();
  493. if (cachedData) {
  494. console.log('使用缓存的犯罪数据');
  495. return cachedData; // 已经在getCachedData中重建了Crime对象
  496. }
  497.  
  498. // 如果没有缓存或缓存过期,从API获取新数据
  499. console.log('从API获取新的犯罪数据');
  500. const newData = await this.fetchCrimeData();
  501. CacheManager.cacheData(newData);
  502. return newData;
  503. }
  504.  
  505. /**
  506. * 从Torn API获取玩家基本信息
  507. * @returns {Promise<Object>} 玩家信息
  508. */
  509. static async fetchPlayerInfo() {
  510. try {
  511. const response = await fetch(`https://api.torn.com/user/?selections=basic&key=${CONFIG.API.KEY}`);
  512. const data = await response.json();
  513. if (data.error) {
  514. throw new Error(`API错误: ${data.error.error}`);
  515. }
  516.  
  517. return data;
  518. } catch (error) {
  519. console.error('获取玩家信息失败:', error);
  520. throw error;
  521. }
  522. }
  523. }
  524.  
  525. // =============== 状态图标管理类 ===============
  526. class StatusIconManager {
  527. constructor(crimeInfo) {
  528. this.crimeInfo = crimeInfo;
  529. }
  530.  
  531. /**
  532. * 检查是否为移动端
  533. * @returns {boolean}
  534. */
  535. isMobileDevice() {
  536. return document.querySelector('.user-information-mobile___WjXnd') !== null;
  537. }
  538.  
  539. /**
  540. * 获取状态容器父元素
  541. * @returns {HTMLElement|null}
  542. */
  543. getStatusContainerParent() {
  544. if (this.isMobileDevice()) {
  545. return document.querySelector('.user-information-mobile___WjXnd');
  546. } else {
  547. return document.getElementsByClassName("status-icons___gPkXF")[0]?.parentNode;
  548. }
  549. }
  550.  
  551. /**
  552. * 更新状态图标
  553. */
  554. updateStatusIcons(userId) {
  555. const containerParent = this.getStatusContainerParent();
  556. if (!containerParent) return;
  557.  
  558. const ocStatusContainer = this.createStatusContainer();
  559. this.removeOldContainer();
  560.  
  561. const userCrime = this.findUserCrime(userId);
  562. if (userCrime) {
  563. this.renderParticipatingStatus(ocStatusContainer, userCrime, userId);
  564. } else {
  565. this.renderNonParticipatingStatus(ocStatusContainer, userId);
  566. }
  567.  
  568. // 移动端时添加额外的样式
  569. if (this.isMobileDevice()) {
  570. ocStatusContainer.style.margin = '10px 15px';
  571. ocStatusContainer.style.width = 'calc(100% - 30px)'; // 考虑左右margin
  572. }
  573.  
  574. containerParent.appendChild(ocStatusContainer);
  575. }
  576.  
  577. /**
  578. * 查找用户参与的犯罪任务
  579. */
  580. findUserCrime(userId) {
  581. return this.crimeInfo.crimes.find(crime =>
  582. crime.slots.some(slot => slot.user_id === userId)
  583. );
  584. }
  585.  
  586. /**
  587. * 创建状态容器
  588. */
  589. createStatusContainer() {
  590. const container = document.createElement('div');
  591. container.style.display = 'flex';
  592. container.style.flexDirection = 'column';
  593. container.style.marginTop = '10px';
  594. container.id = 'oc-status-container';
  595. return container;
  596. }
  597.  
  598. /**
  599. * 移除旧的状态容器
  600. */
  601. removeOldContainer() {
  602. const oldContainer = document.getElementById('oc-status-container');
  603. if (oldContainer) {
  604. oldContainer.remove();
  605. }
  606. }
  607.  
  608. /**
  609. * 渲染参与中的状态
  610. */
  611. renderParticipatingStatus(container, userCrime, userId) {
  612. const slotIcons = this.createSlotIconsContainer();
  613. // 添加点击事件,跳转到对应的OC任务
  614. slotIcons.style.cursor = 'pointer';
  615. slotIcons.addEventListener('click', () => {
  616. window.location.href = `https://www.torn.com/factions.php?step=your#/tab=crimes&crimeId=${userCrime.id}`;
  617. });
  618.  
  619. userCrime.slots.forEach(slot => {
  620. const icon = this.createSlotIcon(slot, userId);
  621. slotIcons.appendChild(icon);
  622. });
  623. container.appendChild(slotIcons);
  624. }
  625.  
  626. /**
  627. * 渲染未参与的状态
  628. */
  629. renderNonParticipatingStatus(container, userId) {
  630. const notInOCContainer = this.createNotInOCContainer();
  631. const textSpan = this.createTextSpan();
  632. const targetCrime = this.findBestAvailableCrime();
  633. const joinLink = this.createJoinLink(targetCrime?.id || '', userId);
  634.  
  635. notInOCContainer.appendChild(textSpan);
  636. notInOCContainer.appendChild(joinLink);
  637. container.appendChild(notInOCContainer);
  638. }
  639.  
  640. /**
  641. * 创建slot图标容器
  642. */
  643. createSlotIconsContainer() {
  644. const container = document.createElement('div');
  645. container.style.display = 'flex';
  646. container.style.alignItems = 'center';
  647. container.style.height = '17px';
  648. container.style.cursor = 'pointer';
  649. // 添加渐变背景和质感效果
  650. container.style.background = 'linear-gradient(to bottom, rgba(30,30,30,0.02) 0%, rgba(0,0,0,0.02) 100%)';
  651. container.style.border = '1px solid rgba(128, 128, 128, 0.2)';
  652. container.style.borderRadius = '3px';
  653. container.style.padding = '3px 5px 3px 0px';
  654. container.style.boxShadow = 'inset 0 1px 0 rgba(255,255,255,0.05), 0 1px 2px rgba(0,0,0,0.02)';
  655. // 添加悬停效果
  656. container.addEventListener('mouseover', () => {
  657. container.style.background = 'linear-gradient(to bottom, rgba(30,30,30,0.04) 0%, rgba(0,0,0,0.04) 100%)';
  658. container.style.boxShadow = 'inset 0 1px 0 rgba(255,255,255,0.08), 0 1px 3px rgba(0,0,0,0.03)';
  659. container.style.transition = 'all 0.2s ease';
  660. });
  661.  
  662. container.addEventListener('mouseout', () => {
  663. container.style.background = 'linear-gradient(to bottom, rgba(30,30,30,0.02) 0%, rgba(0,0,0,0.02) 100%)';
  664. container.style.boxShadow = 'inset 0 1px 0 rgba(255,255,255,0.05), 0 1px 2px rgba(0,0,0,0.02)';
  665. });
  666.  
  667. return container;
  668. }
  669.  
  670. /**
  671. * 创建slot图标
  672. */
  673. createSlotIcon(slot, userId) {
  674. const icon = document.createElement('div');
  675. icon.style.width = '17px';
  676. icon.style.height = '17px';
  677. icon.style.borderRadius = '50%';
  678. icon.style.margin = '5px 10px 5px 0px';
  679. icon.style.boxSizing = 'border-box';
  680. icon.style.display = 'flex';
  681. icon.style.alignItems = 'center';
  682. icon.style.justifyContent = 'center';
  683. icon.style.position = 'relative';
  684. // 添加渐变和阴影效果
  685. if (slot.user) {
  686. // 已加入状态 - 绿色渐变
  687. icon.style.background = 'linear-gradient(135deg, #5cb85c 0%, #4CAF50 100%)';
  688. icon.style.border = '1px solid #45a049';
  689. icon.style.boxShadow = 'inset 0 1px 1px rgba(255,255,255,0.2), 0 1px 2px rgba(0,0,0,0.1)';
  690. } else {
  691. // 空位状态 - 灰色渐变
  692. icon.style.background = 'linear-gradient(135deg, #a4a4a4 0%, #9E9E9E 100%)';
  693. icon.style.border = '1px solid #888';
  694. icon.style.boxShadow = 'inset 0 1px 1px rgba(255,255,255,0.1), 0 1px 2px rgba(0,0,0,0.1)';
  695. }
  696.  
  697. let progressInfo = '';
  698. if (slot.user) {
  699. progressInfo = `${slot.user_id} 在这`;
  700. if (slot.user_id === userId) {
  701. this.addPlayerMarker(icon);
  702. }
  703. } else {
  704. progressInfo = '空位';
  705. }
  706.  
  707. if (slot.item_requirement) {
  708. this.addToolMark(icon);
  709. progressInfo += '\n需要工具';
  710. }
  711.  
  712. icon.title = progressInfo;
  713.  
  714. // 添加悬停效果
  715. icon.addEventListener('mouseover', () => {
  716. icon.style.transform = 'scale(1.1)';
  717. icon.style.transition = 'all 0.2s ease';
  718. icon.style.boxShadow = slot.user
  719. ? 'inset 0 1px 2px rgba(255,255,255,0.3), 0 2px 4px rgba(0,0,0,0.2)'
  720. : 'inset 0 1px 2px rgba(255,255,255,0.2), 0 2px 4px rgba(0,0,0,0.2)';
  721. });
  722.  
  723. icon.addEventListener('mouseout', () => {
  724. icon.style.transform = 'scale(1)';
  725. icon.style.boxShadow = slot.user
  726. ? 'inset 0 1px 1px rgba(255,255,255,0.2), 0 1px 2px rgba(0,0,0,0.1)'
  727. : 'inset 0 1px 1px rgba(255,255,255,0.1), 0 1px 2px rgba(0,0,0,0.1)';
  728. });
  729.  
  730. // 创建自定义tooltip
  731. const tooltip = document.createElement('div');
  732. tooltip.style.position = 'absolute';
  733. tooltip.style.visibility = 'hidden';
  734. tooltip.style.backgroundColor = 'rgba(40, 40, 40, 0.95)';
  735. tooltip.style.color = '#fff';
  736. tooltip.style.padding = '8px 12px';
  737. tooltip.style.borderRadius = '4px';
  738. tooltip.style.fontSize = '12px';
  739. tooltip.style.lineHeight = '1.4';
  740. tooltip.style.whiteSpace = 'nowrap';
  741. tooltip.style.zIndex = '1000';
  742. tooltip.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
  743. tooltip.style.transform = 'translateY(-5px)';
  744. tooltip.style.transition = 'all 0.2s ease';
  745.  
  746. // 设置tooltip内容
  747. let tooltipContent = slot.user
  748. ? `<div style="font-weight:500">${slot.user_id} 在这</div>`
  749. : '<div style="color:#aaa">空位</div>';
  750. if (slot.item_requirement) {
  751. tooltipContent += `
  752. <div style="margin-top:4px;padding-top:4px;border-top:1px solid rgba(255,255,255,0.1)">
  753. <span style="color:#FFA000">⚠</span> 需要工具
  754. </div>`;
  755. }
  756. tooltip.innerHTML = tooltipContent;
  757.  
  758. // 添加tooltip显示/隐藏逻辑
  759. icon.addEventListener('mouseenter', (e) => {
  760. tooltip.style.visibility = 'visible';
  761. tooltip.style.opacity = '1';
  762. tooltip.style.transform = 'translateY(0)';
  763. // 计算位置
  764. const rect = icon.getBoundingClientRect();
  765. tooltip.style.left = rect.left + 'px';
  766. tooltip.style.top = (rect.top - tooltip.offsetHeight - 10) + 'px';
  767. });
  768.  
  769. icon.addEventListener('mouseleave', () => {
  770. tooltip.style.visibility = 'hidden';
  771. tooltip.style.opacity = '0';
  772. tooltip.style.transform = 'translateY(-5px)';
  773. });
  774.  
  775. document.body.appendChild(tooltip);
  776. icon.title = ''; // 移除默认tooltip
  777. return icon;
  778. }
  779.  
  780. /**
  781. * 添加玩家标记
  782. */
  783. addPlayerMarker(icon) {
  784. const marker = document.createElement('span');
  785. marker.innerHTML = '★';
  786. marker.style.color = 'white';
  787. marker.style.fontSize = '10px';
  788. marker.style.textShadow = '0 0 1px #000';
  789. icon.appendChild(marker);
  790. }
  791.  
  792. /**
  793. * 添加工具标记
  794. */
  795. addToolMark(icon) {
  796. const toolMark = document.createElement('div');
  797. toolMark.style.position = 'absolute';
  798. toolMark.style.bottom = '0';
  799. toolMark.style.right = '0';
  800. toolMark.style.width = '6px';
  801. toolMark.style.height = '6px';
  802. toolMark.style.backgroundColor = '#FFC107';
  803. toolMark.style.borderRadius = '50%';
  804. toolMark.style.border = '1px solid #FFA000';
  805. toolMark.style.transform = 'translate(25%, 25%)';
  806. icon.appendChild(toolMark);
  807. }
  808.  
  809. /**
  810. * 创建未参加OC的容器
  811. */
  812. createNotInOCContainer() {
  813. const container = document.createElement('div');
  814. container.style.display = 'flex';
  815. container.style.alignItems = 'center';
  816. container.style.gap = '5px';
  817. container.style.backgroundColor = '#F44336';
  818. container.style.padding = '3px 8px';
  819. container.style.borderRadius = '3px';
  820. container.style.marginBottom = '10px';
  821. return container;
  822. }
  823.  
  824. /**
  825. * 创建提示文本
  826. */
  827. createTextSpan() {
  828. const textSpan = document.createElement('span');
  829. textSpan.textContent = '未加入oc,';
  830. textSpan.style.fontSize = '12px';
  831. textSpan.style.color = 'white';
  832. return textSpan;
  833. }
  834.  
  835. /**
  836. * 查找最佳可用的犯罪任务
  837. */
  838. findBestAvailableCrime() {
  839. let targetCrime = this.crimeInfo.crimes.find(crime =>
  840. crime.isMissingUser()
  841. );
  842.  
  843. if (!targetCrime) {
  844. const emptyCrimes = this.crimeInfo.crimes.filter(crime =>
  845. !crime.isUserd()
  846. );
  847.  
  848. if (emptyCrimes.length > 0) {
  849. targetCrime = emptyCrimes.reduce((highest, current) =>
  850. current.difficulty > highest.difficulty ? current : highest
  851. );
  852. } else {
  853. const availableCrimes = this.crimeInfo.crimes.filter(crime =>
  854. crime.slots.some(slot => !slot.user)
  855. );
  856. targetCrime = availableCrimes.reduce((highest, current) =>
  857. current.difficulty > highest.difficulty ? current : highest
  858. );
  859. }
  860. }
  861.  
  862. return targetCrime;
  863. }
  864.  
  865. /**
  866. * 创建加入链接
  867. */
  868. createJoinLink(crimeId, userId) {
  869. const joinLink = document.createElement('a');
  870. joinLink.textContent = 'join';
  871. joinLink.href = `https://www.torn.com/factions.php?step=your#/tab=crimes&crimeId=${crimeId}`;
  872. joinLink.style.color = 'white';
  873. joinLink.style.textDecoration = 'underline';
  874. joinLink.style.fontSize = '13px';
  875. joinLink.style.fontWeight = 'bold';
  876. joinLink.style.textShadow = '0 0 1px rgba(255, 255, 255, 0.5)';
  877. joinLink.style.letterSpacing = '0.5px';
  878.  
  879. this.addJoinLinkEffects(joinLink, userId);
  880. return joinLink;
  881. }
  882.  
  883. /**
  884. * 添加加入链接效果
  885. */
  886. addJoinLinkEffects(joinLink, userId) {
  887. joinLink.addEventListener('mouseover', () => {
  888. joinLink.style.textShadow = '0 0 2px rgba(255, 255, 255, 0.8)';
  889. joinLink.style.transition = 'all 0.2s ease';
  890. });
  891.  
  892. joinLink.addEventListener('mouseout', () => {
  893. joinLink.style.textShadow = '0 0 1px rgba(255, 255, 255, 0.5)';
  894. });
  895.  
  896. joinLink.addEventListener('click', async () => {
  897. try {
  898. const newData = await APIManager.fetchCrimeData();
  899. this.crimeInfo = newData;
  900. const playerInfo = await APIManager.fetchPlayerInfo();
  901. this.updateStatusIcons(playerInfo.player_id);
  902. } catch (error) {
  903. console.error('更新OC数据失败:', error);
  904. }
  905. });
  906. }
  907. }
  908.  
  909. // =============== 主程序类 ===============
  910. class OCFacilitation {
  911. constructor() {
  912. this.crimeInfo = null;
  913. this.currentTab = null;
  914. this.isInitialized = false;
  915. this.isUpdating = false;
  916. this.observer = null;
  917. this.statusIconManager = null;
  918. }
  919.  
  920. /**
  921. * 处理页面变化
  922. */
  923. async handlePageChange() {
  924. if (!Utils.isFactionPage() || !Utils.isOCPage()) {
  925. this.cleanup();
  926. return;
  927. }
  928.  
  929. try {
  930. if (!this.crimeInfo) {
  931. this.crimeInfo = await APIManager.fetchCrimeData();
  932. }
  933. const container = await Utils.waitForWrapper();
  934. await this.handleInitialUIUpdate(container);
  935. // 如果还没有设置观察器,设置它
  936. if (!this.observer) {
  937. this.setupObserver(container);
  938. }
  939. } catch (error) {
  940. console.error('处理页面变化失败:', error);
  941. }
  942. }
  943.  
  944. /**
  945. * 处理初始UI更新
  946. * @param {HTMLElement} container - 犯罪任务列表容器
  947. */
  948. async handleInitialUIUpdate(container) {
  949. await new Promise(resolve => setTimeout(resolve, CONFIG.UI.LOAD_DELAY));
  950. await this.updateCrimeListUI(container);
  951. }
  952.  
  953. /**
  954. * 更新犯罪任务列表UI
  955. * @param {HTMLElement} container - 犯罪任务列表容器
  956. */
  957. async updateCrimeListUI(container) {
  958. if (this.isUpdating) return;
  959.  
  960. try {
  961. this.isUpdating = true;
  962. CrimeUIManager.updateAllCrimesUI(container);
  963. } finally {
  964. this.isUpdating = false;
  965. }
  966. }
  967.  
  968. /**
  969. * 设置观察器
  970. * @param {HTMLElement} container - 犯罪任务列表容器
  971. */
  972. setupObserver(container) {
  973. this.observer = new MutationObserver(Utils.debounce((mutations) => {
  974. const hasChildrenChanges = mutations.some(mutation =>
  975. mutation.type === 'childList' &&
  976. mutation.target === container
  977. );
  978.  
  979. if (hasChildrenChanges) {
  980. this.updateCrimeListUI(container)
  981. .catch(error => console.error('更新犯罪任务UI失败:', error));
  982. }
  983. }, CONFIG.UI.UPDATE_DEBOUNCE));
  984.  
  985. this.observer.observe(container, {
  986. childList: true,
  987. subtree: false,
  988. attributes: false,
  989. characterData: false
  990. });
  991. }
  992.  
  993. /**
  994. * 清理资源
  995. */
  996. cleanup() {
  997. if (this.observer) {
  998. this.observer.disconnect();
  999. this.observer = null;
  1000. }
  1001. this.isUpdating = false;
  1002. }
  1003.  
  1004. /**
  1005. * 初始化程序
  1006. */
  1007. async initialize() {
  1008. try {
  1009. await this.initializeData();
  1010. await this.setupStatusIcons();
  1011. await this.setupPageChangeListeners();
  1012. this.isInitialized = true;
  1013. } catch (error) {
  1014. console.error('初始化失败:', error);
  1015. }
  1016. }
  1017.  
  1018. /**
  1019. * 初始化数据
  1020. */
  1021. async initializeData() {
  1022. // 直接从API获取新数据
  1023. this.crimeInfo = await APIManager.fetchCrimeData();
  1024. this.statusIconManager = new StatusIconManager(this.crimeInfo);
  1025. }
  1026.  
  1027. /**
  1028. * 设置UI
  1029. */
  1030. async setupStatusIcons() {
  1031. // 获取玩家信息并更新状态图标
  1032. const playerInfo = await APIManager.fetchPlayerInfo();
  1033. this.statusIconManager.updateStatusIcons(playerInfo.player_id);
  1034. }
  1035.  
  1036. /**
  1037. * 设置页面变化监听器
  1038. */
  1039. setupPageChangeListeners() {
  1040. // 监听hash变化(页签切换)
  1041. window.addEventListener('hashchange', () => this.handlePageChange());
  1042. // 监听页面加载完成
  1043. if (document.readyState === 'complete') {
  1044. this.handlePageChange();
  1045. } else {
  1046. window.addEventListener('load', () => this.handlePageChange());
  1047. }
  1048. }
  1049. }
  1050.  
  1051. // 启动程序
  1052. (() => {
  1053. const app = new OCFacilitation();
  1054. app.initialize();
  1055. // 页面卸载时清理资源
  1056. window.addEventListener('unload', () => {
  1057. app.cleanup();
  1058. });
  1059. })();
  1060. })();

QingJ © 2025

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