Torn Item Safety

Displays effects of items in Torn's various item shops with hover tooltips and disables items with warnings when toggle is active.

  1. // ==UserScript==
  2. // @name Torn Item Safety
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.1.1
  5. // @license GNU GPLv3
  6. // @description Displays effects of items in Torn's various item shops with hover tooltips and disables items with warnings when toggle is active.
  7. // @author Vassilios [2276659]
  8. // @match https://www.torn.com/item.php*
  9. // @match https://www.torn.com/bigalgunshop.php*
  10. // @match https://www.torn.com/shops.php?step=*
  11. // @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM_registerMenuCommand
  14. // @connect api.torn.com
  15. // ==/UserScript==
  16.  
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. // Register Tampermonkey menu command to enter API key
  22. GM_registerMenuCommand('Enter API key.', () => {
  23. const newApiKey = prompt('Please enter your Torn API key:', '');
  24. if (newApiKey && newApiKey.trim() !== '') {
  25. localStorage.setItem(CONFIG.STORAGE_KEYS.API_KEY, newApiKey.trim());
  26. // Clear cached data to force a fresh fetch with the new key
  27. localStorage.removeItem(CONFIG.STORAGE_KEYS.ITEM_DATA);
  28. localStorage.removeItem(CONFIG.STORAGE_KEYS.LAST_REQUEST_TIME);
  29. // Trigger a refresh of item data
  30. ItemEffectsApp.init();
  31. }
  32. });
  33.  
  34. // =====================================================================
  35. // CONFIGURATION
  36. // =====================================================================
  37.  
  38. const CONFIG = {
  39. getApiKey: () => localStorage.getItem('tornItemEffects_apiKey') || "", // Dynamically retrieve API key
  40. API_BASE_URL: 'https://api.torn.com/v2/torn/items',
  41. ITEM_CATEGORIES: ['Tool', 'Enhancer'],
  42. CACHE_DURATION: 24 * 60 * 60 * 1000, // 24 hours in milliseconds
  43. OBSERVER_CONFIG: { childList: true, subtree: true },
  44. ELEMENT_IDS: {
  45. WARNINGS_TOGGLE: 'warnings-toggle',
  46. WARNINGS_STATUS: 'warnings-status'
  47. },
  48. SELECTORS: {
  49. ALL_ITEMS: '#all-items',
  50. SELL_ITEMS_WRAP: '.sell-items-wrap',
  51. CONTENT_TITLE_LINKS: '.content-title-links',
  52. ITEM_NAME: '.name',
  53. WARNING_SIGN: '.warning-sign'
  54. },
  55. STORAGE_KEYS: {
  56. WARNING_STATE: 'tornItemEffects_warningState',
  57. ITEM_DATA: 'tornItemEffects_itemData',
  58. LAST_REQUEST_TIME: 'tornItemEffects_lastRequestTime',
  59. API_KEY: 'tornItemEffects_apiKey'
  60. },
  61. STYLES: {
  62. WARNING_SIGN: {
  63. color: '#ff9900',
  64. fontWeight: 'bold',
  65. marginLeft: '5px',
  66. cursor: 'help'
  67. },
  68. TOOLTIP: {
  69. position: 'absolute',
  70. backgroundColor: '#191919',
  71. color: '#F7FAFC',
  72. padding: '6px 10px',
  73. borderRadius: '4px',
  74. fontSize: '12px',
  75. zIndex: '9999999',
  76. display: 'none',
  77. boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
  78. maxWidth: '250px',
  79. textAlign: 'left',
  80. fontFamily: 'Segoe UI, Arial, sans-serif',
  81. lineHeight: '1.3',
  82. border: '1px solid #4A5568',
  83. wordWrap: 'break-word',
  84. overflowWrap: 'break-word'
  85. },
  86. STATUS_INDICATOR: {
  87. ON: { text: 'ON', color: '#4CAF50' },
  88. OFF: { text: 'OFF', color: '#F44336' }
  89. }
  90. }
  91. };
  92.  
  93. // =====================================================================
  94. // STATE MANAGEMENT
  95. // =====================================================================
  96.  
  97. const State = {
  98. itemData: [],
  99. observers: { items: null, body: null, disabledInputs: null },
  100. disabledInputs: new Map() // Store references to disabled inputs
  101. };
  102.  
  103. // =====================================================================
  104. // API INTERFACE
  105. // =====================================================================
  106.  
  107. const ApiService = {
  108. fetchItemCategory: function(category) {
  109. return new Promise((resolve, reject) => {
  110. if (typeof GM_xmlhttpRequest === 'undefined') {
  111. console.error('GM_xmlhttpRequest is not available');
  112. reject(new Error('GM_xmlhttpRequest is not defined'));
  113. return;
  114. }
  115.  
  116. const apiKey = CONFIG.getApiKey();
  117. const url = `${CONFIG.API_BASE_URL}?cat=${category}&sort=ASC`;
  118. GM_xmlhttpRequest({
  119. method: 'GET',
  120. url: url,
  121. headers: {
  122. 'accept': 'application/json',
  123. 'Authorization': `ApiKey ${apiKey}`
  124. },
  125. onload: function(response) {
  126. try {
  127. const data = JSON.parse(response.responseText);
  128. if (data && data.items) {
  129. resolve(data.items);
  130. } else {
  131. console.error('API response does not contain items:', response.responseText);
  132. reject(new Error('Invalid data format from API'));
  133. }
  134. } catch (error) {
  135. console.error('Error parsing API response:', response.responseText, error);
  136. reject(error);
  137. }
  138. },
  139. onerror: function(error) {
  140. console.error('GM_xmlhttpRequest error:', error);
  141. reject(error);
  142. }
  143. });
  144. });
  145. },
  146.  
  147. fetchAllItemData: function() {
  148. // Check if API key is set
  149. const apiKey = CONFIG.getApiKey();
  150. if (!apiKey) {
  151. const cachedData = localStorage.getItem(CONFIG.STORAGE_KEYS.ITEM_DATA);
  152. if (cachedData) {
  153. try {
  154. State.itemData = JSON.parse(cachedData);
  155. return Promise.resolve(State.itemData);
  156. } catch (error) {
  157. console.error('Error parsing cached data:', error);
  158. return Promise.resolve([]);
  159. }
  160. }
  161. return Promise.resolve([]);
  162. }
  163.  
  164. // Check for cached data
  165. const cachedData = localStorage.getItem(CONFIG.STORAGE_KEYS.ITEM_DATA);
  166. const lastRequestTime = localStorage.getItem(CONFIG.STORAGE_KEYS.LAST_REQUEST_TIME);
  167. const currentTime = Date.now();
  168.  
  169. if (cachedData && lastRequestTime && cachedData !== "[]") {
  170. const timeSinceLastRequest = currentTime - parseInt(lastRequestTime, 10);
  171. if (timeSinceLastRequest < CONFIG.CACHE_DURATION) {
  172. try {
  173. State.itemData = JSON.parse(cachedData);
  174. return Promise.resolve(State.itemData);
  175. } catch (error) {
  176. console.error('Error parsing cached data:', error);
  177. }
  178. }
  179. }
  180.  
  181. // Fetch new data
  182. const fetchPromises = CONFIG.ITEM_CATEGORIES.map(category =>
  183. this.fetchItemCategory(category)
  184. .then(items => {
  185. const simplifiedItems = items.map(item => ({
  186. name: item.name,
  187. effect: item.effect
  188. }));
  189. State.itemData = [...State.itemData, ...simplifiedItems];
  190. return simplifiedItems;
  191. })
  192. .catch(error => {
  193. console.error(`Error fetching ${category} items:`, error);
  194. return [];
  195. })
  196. );
  197.  
  198. return Promise.all(fetchPromises).then(() => {
  199. // Save to localStorage
  200. try {
  201. localStorage.setItem(CONFIG.STORAGE_KEYS.ITEM_DATA, JSON.stringify(State.itemData));
  202. localStorage.setItem(CONFIG.STORAGE_KEYS.LAST_REQUEST_TIME, currentTime.toString());
  203. } catch (error) {
  204. console.error('Error saving to localStorage:', error);
  205. }
  206. return State.itemData;
  207. });
  208. }
  209. };
  210.  
  211. // =====================================================================
  212. // DOM UTILITIES
  213. // =====================================================================
  214.  
  215. const DomUtils = {
  216. find: {
  217. itemContainers: function() {
  218. const containers = [];
  219. const allItemsList = document.querySelector(CONFIG.SELECTORS.ALL_ITEMS);
  220. if (allItemsList) containers.push(allItemsList);
  221. const sellItemsWrap = document.querySelector(CONFIG.SELECTORS.SELL_ITEMS_WRAP);
  222. if (sellItemsWrap) containers.push(sellItemsWrap);
  223. return containers;
  224. },
  225.  
  226. listItems: function() {
  227. const containers = this.itemContainers();
  228. let items = [];
  229. containers.forEach(container => {
  230. const containerItems = Array.from(container.getElementsByTagName('li'));
  231. items = [...items, ...containerItems];
  232. });
  233. return items;
  234. },
  235.  
  236. navigationContainer: function() {
  237. return document.querySelector(CONFIG.SELECTORS.CONTENT_TITLE_LINKS);
  238. }
  239. },
  240.  
  241. create: {
  242. warningSign: function(effectText) {
  243. const warningSign = document.createElement('span');
  244. warningSign.className = 'warning-sign';
  245. Object.assign(warningSign.style, CONFIG.STYLES.WARNING_SIGN);
  246. warningSign.innerHTML = '⚠️';
  247.  
  248. const tooltip = this.tooltip(effectText);
  249. warningSign.appendChild(tooltip);
  250.  
  251. warningSign.addEventListener('mouseenter', function() {
  252. tooltip.style.display = 'block';
  253. const rect = warningSign.getBoundingClientRect();
  254. const isLongText = (tooltip.textContent || '').length > 50;
  255. tooltip.style.left = '0px';
  256. tooltip.style.top = isLongText ? '-45px' : '-30px';
  257.  
  258. setTimeout(() => {
  259. const tooltipRect = tooltip.getBoundingClientRect();
  260. if (tooltipRect.left < 0) tooltip.style.left = '5px';
  261. if (tooltipRect.top < 0) tooltip.style.top = '20px';
  262. }, 0);
  263. });
  264.  
  265. warningSign.addEventListener('mouseleave', function() {
  266. tooltip.style.display = 'none';
  267. });
  268.  
  269. return warningSign;
  270. },
  271.  
  272. tooltip: function(content) {
  273. const tooltip = document.createElement('div');
  274. tooltip.className = 'item-effect-tooltip';
  275. tooltip.setAttribute('role', 'tooltip');
  276. Object.assign(tooltip.style, CONFIG.STYLES.TOOLTIP);
  277. tooltip.textContent = content;
  278. return tooltip;
  279. },
  280.  
  281. toggleButton: function() {
  282. const toggleButton = document.createElement('a');
  283. toggleButton.id = CONFIG.ELEMENT_IDS.WARNINGS_TOGGLE;
  284. toggleButton.className = 'warnings-toggle t-clear h c-pointer m-icon line-h24 right';
  285. toggleButton.setAttribute('aria-labelledby', 'warnings-toggle-label');
  286.  
  287. const savedState = localStorage.getItem(CONFIG.STORAGE_KEYS.WARNING_STATE);
  288. const isActive = savedState !== null ? savedState === 'true' : true;
  289.  
  290. if (isActive) {
  291. toggleButton.classList.add('top-page-link-button--active');
  292. toggleButton.classList.add('active');
  293. }
  294.  
  295. toggleButton.innerHTML = `
  296. <span class="icon-wrap svg-icon-wrap">
  297. <span class="link-icon-svg warnings-icon">
  298. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 16">
  299. <defs>
  300. <style>.cls-1{opacity:0.35;}.cls-2{fill:#fff;}.cls-3{fill:#777;}</style>
  301. </defs>
  302. <g>
  303. <g>
  304. <g class="cls-1">
  305. <path class="cls-2" d="M7.5,1 L15,15 L0,15 Z"></path>
  306. </g>
  307. <path class="cls-3" d="M7.5,0 L15,14 L0,14 Z"></path>
  308. <path class="cls-3" d="M7,6 L8,6 L8,10 L7,10 Z" style="fill:#fff"></path>
  309. <circle class="cls-3" cx="7.5" cy="12" r="1" style="fill:#fff"></circle>
  310. </g>
  311. </g>
  312. </svg>
  313. </span>
  314. </span>
  315. <span id="warnings-toggle-label">Item Safety:</span>
  316. `;
  317.  
  318. const statusIndicator = document.createElement('span');
  319. statusIndicator.id = CONFIG.ELEMENT_IDS.WARNINGS_STATUS;
  320. statusIndicator.style.marginLeft = '5px';
  321. statusIndicator.style.fontWeight = 'bold';
  322. statusIndicator.textContent = isActive ? CONFIG.STYLES.STATUS_INDICATOR.ON.text : CONFIG.STYLES.STATUS_INDICATOR.OFF.text;
  323. statusIndicator.style.color = isActive ? CONFIG.STYLES.STATUS_INDICATOR.ON.color : CONFIG.STYLES.STATUS_INDICATOR.OFF.color;
  324. toggleButton.appendChild(statusIndicator);
  325.  
  326. return toggleButton;
  327. }
  328. }
  329. };
  330.  
  331. // =====================================================================
  332. // INPUT PROTECTION FUNCTIONALITY
  333. // =====================================================================
  334.  
  335. const InputProtection = {
  336. setupDisabledInputsObserver: function() {
  337. // Create a MutationObserver to watch for changes to disabled inputs
  338. const observerConfig = {
  339. attributes: true,
  340. attributeFilter: ['disabled', 'value'],
  341. subtree: true
  342. };
  343.  
  344. State.observers.disabledInputs = new MutationObserver(mutations => {
  345. mutations.forEach(mutation => {
  346. const element = mutation.target;
  347.  
  348. // Check if warnings are enabled before applying protection
  349. const warningsEnabled = localStorage.getItem(CONFIG.STORAGE_KEYS.WARNING_STATE) !== 'false';
  350. if (!warningsEnabled) return;
  351.  
  352. // Handle disabled attribute changes
  353. if (mutation.attributeName === 'disabled') {
  354. if (!element.disabled && element.dataset.disabledByWarning === 'true') {
  355. // Element was disabled by our script but something tried to enable it
  356. // Re-disable it
  357. element.disabled = true;
  358. }
  359. }
  360.  
  361. // Handle value changes on disabled inputs
  362. if (mutation.attributeName === 'value' && element.disabled && element.dataset.disabledByWarning === 'true') {
  363. // Reset value to 0 if it was changed while disabled
  364. if (element.value !== '0') {
  365. element.value = '0';
  366. }
  367. }
  368. });
  369. });
  370.  
  371. document.addEventListener('input', function(e) {
  372. // Check if warnings are enabled before applying protection
  373. const warningsEnabled = localStorage.getItem(CONFIG.STORAGE_KEYS.WARNING_STATE) !== 'false';
  374. if (!warningsEnabled) return;
  375.  
  376. // For any input events on disabled inputs
  377. if (e.target.disabled && e.target.dataset.disabledByWarning === 'true') {
  378. e.preventDefault();
  379. e.stopPropagation();
  380. e.target.value = '0';
  381. }
  382. }, true);
  383.  
  384. // Start observing the entire document
  385. State.observers.disabledInputs.observe(document.documentElement, observerConfig);
  386. },
  387.  
  388. // Proxy for input properties to intercept changes to disabled inputs
  389. setupInputPropertyProxy: function() {
  390. // Store original property descriptors
  391. const originalValueDescriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
  392. const originalDisabledDescriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'disabled');
  393.  
  394. // Override the value property
  395. Object.defineProperty(HTMLInputElement.prototype, 'value', {
  396. set: function(newValue) {
  397. // Check if warnings are enabled before applying protection
  398. const warningsEnabled = localStorage.getItem(CONFIG.STORAGE_KEYS.WARNING_STATE) !== 'false';
  399.  
  400. if (warningsEnabled && this.disabled && this.dataset.disabledByWarning === 'true') {
  401. originalValueDescriptor.set.call(this, '0');
  402. return '0';
  403. } else {
  404. return originalValueDescriptor.set.call(this, newValue);
  405. }
  406. },
  407. get: function() {
  408. return originalValueDescriptor.get.call(this);
  409. },
  410. configurable: true
  411. });
  412.  
  413. // Override the disabled property
  414. Object.defineProperty(HTMLInputElement.prototype, 'disabled', {
  415. set: function(value) {
  416. // Check if warnings are enabled before applying protection
  417. const warningsEnabled = localStorage.getItem(CONFIG.STORAGE_KEYS.WARNING_STATE) !== 'false';
  418.  
  419. if (warningsEnabled && !value && this.dataset.disabledByWarning === 'true') {
  420. return originalDisabledDescriptor.set.call(this, true);
  421. } else {
  422. return originalDisabledDescriptor.set.call(this, value);
  423. }
  424. },
  425. get: function() {
  426. return originalDisabledDescriptor.get.call(this);
  427. },
  428. configurable: true
  429. });
  430. },
  431.  
  432. // Method to track new disabled inputs
  433. trackDisabledInput: function(input) {
  434. if (input.type === 'text' || input.type === 'number') {
  435. // Store original value
  436. input.dataset.originalValue = input.value || '';
  437.  
  438. // Set value to 0 for numerical inputs
  439. if (input.type === 'number' || !isNaN(parseFloat(input.value))) {
  440. input.value = '0';
  441. }
  442. }
  443.  
  444. // Add to our tracking map
  445. State.disabledInputs.set(input, {
  446. originalDisabled: input.disabled,
  447. originalValue: input.dataset.originalValue
  448. });
  449. },
  450.  
  451. // Method to untrack and restore inputs when warnings are disabled
  452. restoreInput: function(input) {
  453. // Restore original value if it exists
  454. if (input.dataset.originalValue !== undefined) {
  455. input.value = input.dataset.originalValue;
  456. delete input.dataset.originalValue;
  457. }
  458.  
  459. // Restore original state
  460. input.disabled = false;
  461. input.style.opacity = '';
  462. input.style.cursor = '';
  463.  
  464. delete input.dataset.disabledByWarning;
  465.  
  466. if (input.type === 'text' && input.dataset.originalBg !== undefined) {
  467. input.style.backgroundColor = input.dataset.originalBg;
  468. delete input.dataset.originalBg;
  469. }
  470.  
  471. // Remove from tracking
  472. State.disabledInputs.delete(input);
  473. },
  474.  
  475. // Initialize input protection
  476. init: function() {
  477. this.setupDisabledInputsObserver();
  478. this.setupInputPropertyProxy();
  479. }
  480. };
  481.  
  482. // =====================================================================
  483. // CORE FUNCTIONALITY
  484. // =====================================================================
  485.  
  486. const ItemEffectsApp = {
  487. init: function() {
  488. this.initializeToggleButton();
  489.  
  490. // Initialize input protection first
  491. InputProtection.init();
  492.  
  493. ApiService.fetchAllItemData()
  494. .then(() => {
  495. this.processItems();
  496. this.setupObservers();
  497. this.applyWarningState(); // Changed from applyInitialWarningState to be more general
  498. })
  499. .catch(error => {
  500. console.error('Failed to fetch item data:', error);
  501. this.processItems();
  502. this.setupObservers();
  503. this.applyWarningState(); // Changed from applyInitialWarningState to be more general
  504. });
  505. },
  506.  
  507. applyWarningState: function() {
  508. const savedState = localStorage.getItem(CONFIG.STORAGE_KEYS.WARNING_STATE);
  509. const isActive = savedState !== null ? savedState === 'true' : true;
  510. const warningElements = document.querySelectorAll(CONFIG.SELECTORS.WARNING_SIGN);
  511.  
  512. warningElements.forEach(warning => {
  513. const listItem = warning.closest('li[data-item]');
  514. if (!listItem) return;
  515.  
  516. const isItemPage = window.location.href.includes('item.php');
  517.  
  518. if (isItemPage) {
  519. const deleteButtons = listItem.querySelectorAll('.option-delete');
  520. deleteButtons.forEach(button => {
  521. if (isActive) {
  522. button.disabled = true;
  523. button.style.opacity = '0.5';
  524. button.style.cursor = 'not-allowed';
  525. button.dataset.disabledByWarning = 'true';
  526. } else {
  527. button.disabled = false;
  528. button.style.opacity = '';
  529. button.style.cursor = '';
  530. delete button.dataset.disabledByWarning;
  531. }
  532. });
  533. } else {
  534. const inputElements = listItem.querySelectorAll('input, button, select, textarea, a.buy');
  535. inputElements.forEach(input => {
  536. if (isActive) {
  537. if (input.tagName.toLowerCase() === 'a') {
  538. input.dataset.originalHref = input.href;
  539. input.href = 'javascript:void(0)';
  540. input.style.opacity = '0.5';
  541. input.style.pointerEvents = 'none';
  542. } else {
  543. // Store original value before disabling
  544. if (input.type === 'text' || input.type === 'number') {
  545. input.dataset.originalValue = input.value || '';
  546. }
  547.  
  548. input.disabled = true;
  549. input.style.opacity = '0.5';
  550. input.style.cursor = 'not-allowed';
  551. input.dataset.disabledByWarning = 'true';
  552.  
  553. // Track and protect this disabled input
  554. InputProtection.trackDisabledInput(input);
  555.  
  556. if (input.type === 'text') {
  557. input.dataset.originalBg = input.style.backgroundColor;
  558. input.style.backgroundColor = '#e0e0e0';
  559. }
  560. }
  561. } else {
  562. if (input.tagName.toLowerCase() === 'a') {
  563. if (input.dataset.originalHref) {
  564. input.href = input.dataset.originalHref;
  565. }
  566. input.style.opacity = '';
  567. input.style.pointerEvents = '';
  568. } else {
  569. // Use the dedicated restore method
  570. InputProtection.restoreInput(input);
  571. }
  572. }
  573. });
  574. }
  575. });
  576. },
  577.  
  578. processItems: function() {
  579. const listItems = DomUtils.find.listItems();
  580.  
  581. if (listItems.length === 0) return;
  582.  
  583. listItems.forEach(item => this.processItemElement(item));
  584.  
  585. // After processing items, apply the warning state based on toggle setting
  586. setTimeout(() => this.applyWarningState(), 0);
  587. },
  588.  
  589. processItemElement: function(itemElement) {
  590. const nameElement = itemElement.querySelector(CONFIG.SELECTORS.ITEM_NAME);
  591. if (!nameElement || !nameElement.textContent) return;
  592.  
  593. const itemName = nameElement.textContent.trim();
  594. const matchingItem = State.itemData.find(item => item.name === itemName);
  595.  
  596. if (matchingItem && matchingItem.effect && !nameElement.querySelector(CONFIG.SELECTORS.WARNING_SIGN)) {
  597. const warningSign = DomUtils.create.warningSign(matchingItem.effect);
  598. nameElement.appendChild(warningSign);
  599. }
  600. },
  601.  
  602. initializeToggleButton: function() {
  603. if (document.getElementById(CONFIG.ELEMENT_IDS.WARNINGS_TOGGLE)) {
  604. return;
  605. }
  606.  
  607. const navContainer = DomUtils.find.navigationContainer();
  608. if (!navContainer) {
  609. if (document.readyState !== 'complete' && document.readyState !== 'interactive') {
  610. document.addEventListener('DOMContentLoaded', () => this.initializeToggleButton());
  611. }
  612. return;
  613. }
  614.  
  615. const toggleButton = DomUtils.create.toggleButton();
  616. toggleButton.addEventListener('click', this.toggleWarnings);
  617. navContainer.appendChild(toggleButton);
  618. },
  619.  
  620. toggleWarnings: function() {
  621. this.classList.toggle('top-page-link-button--active');
  622. this.classList.toggle('active');
  623.  
  624. const warningsEnabled = this.classList.contains('active');
  625. localStorage.setItem(CONFIG.STORAGE_KEYS.WARNING_STATE, warningsEnabled);
  626.  
  627. const statusIndicator = document.getElementById(CONFIG.ELEMENT_IDS.WARNINGS_STATUS);
  628. statusIndicator.textContent = warningsEnabled ? CONFIG.STYLES.STATUS_INDICATOR.ON.text : CONFIG.STYLES.STATUS_INDICATOR.OFF.text;
  629. statusIndicator.style.color = warningsEnabled ? CONFIG.STYLES.STATUS_INDICATOR.ON.color : CONFIG.STYLES.STATUS_INDICATOR.OFF.color;
  630.  
  631. // Use the general applyWarningState method instead of duplicating logic here
  632. ItemEffectsApp.applyWarningState();
  633. },
  634.  
  635. setupObservers: function() {
  636. const itemContainers = DomUtils.find.itemContainers();
  637. State.observers.items = new MutationObserver(mutations => {
  638. let newItemsAdded = false;
  639. mutations.forEach(mutation => {
  640. if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
  641. newItemsAdded = true;
  642. }
  643. });
  644. if (newItemsAdded) {
  645. this.processItems();
  646. // Apply warning state after new items are processed
  647. this.applyWarningState();
  648. }
  649. });
  650.  
  651. if (itemContainers.length > 0) {
  652. itemContainers.forEach(container => {
  653. State.observers.items.observe(container, CONFIG.OBSERVER_CONFIG);
  654. });
  655. } else {
  656. this.setupBodyObserver();
  657. }
  658. },
  659.  
  660. setupBodyObserver: function() {
  661. State.observers.body = new MutationObserver(mutations => {
  662. const itemContainers = DomUtils.find.itemContainers();
  663. if (itemContainers.length > 0) {
  664. itemContainers.forEach(container => {
  665. State.observers.items.observe(container, CONFIG.OBSERVER_CONFIG);
  666. });
  667. this.processItems();
  668. this.initializeToggleButton();
  669. // Apply warning state after container is found
  670. this.applyWarningState();
  671. State.observers.body.disconnect();
  672. }
  673. });
  674. State.observers.body.observe(document.body, CONFIG.OBSERVER_CONFIG);
  675. }
  676. };
  677.  
  678. // =====================================================================
  679. // INITIALIZATION
  680. // =====================================================================
  681.  
  682. if (document.readyState === 'complete' || document.readyState === 'interactive') {
  683. ItemEffectsApp.init();
  684. } else {
  685. document.addEventListener('DOMContentLoaded', () => ItemEffectsApp.init());
  686. }
  687.  
  688. window.TornItemEffects = {
  689. processItems: () => ItemEffectsApp.processItems(),
  690. toggleWarnings: () => {
  691. const warningToggleButton = document.getElementById(CONFIG.ELEMENT_IDS.WARNINGS_TOGGLE);
  692. if (warningToggleButton) warningToggleButton.click();
  693. }
  694. };
  695. })();