Torn NPC Attack Time Newsfeed

Add NPC attack time to the news ticker using Loot Rangers for Torn

  1. // ==UserScript==
  2. // @name Torn NPC Attack Time Newsfeed
  3. // @namespace npc.timing
  4. // @version v1.1.5
  5. // @description Add NPC attack time to the news ticker using Loot Rangers for Torn
  6. // @author IceBlueFire [776]
  7. // @license MIT
  8. // @match https://www.torn.com/*
  9. // @exclude https://www.torn.com/newspaper.php
  10. // @exclude https://www.torn.com/item.php
  11. // @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
  12. // @grant unsafeWindow
  13. // @grant GM_xmlhttpRequest
  14. // @require https://code.jquery.com/jquery-1.8.2.min.js
  15. // @connect api.lzpt.io
  16. // ==/UserScript==
  17.  
  18. /******************** CONFIG SETTINGS ********************/
  19. const color = "#8abeef"; // Any hex-code for the color to appear in the news feed as
  20. const format = 24; // Time format. 12 = 12:00 AM format; 24 = 23:59 format
  21. const local = false; // Adjust the timer to be local time or not. true = local; false = UTC
  22.  
  23. /****************** END CONFIG SETTINGS *******************/
  24.  
  25. const lzpt = getAttackTimes();
  26. const { fetch: originalFetch } = unsafeWindow;
  27. unsafeWindow.fetch = async (...args) => {
  28. var [resource, config] = args;
  29. var response = await originalFetch(resource, config);
  30. if(response.url.indexOf('?sid=newsTicker') === -1) return response;
  31. const json = () => response.clone().json()
  32. .then((data) => {
  33. data = { ...data };
  34. lzpt.then(function(result) {
  35. var attackOrder = '';
  36. var attackString = '';
  37. var attackLink = '';
  38. var attackTarget = 0;
  39.  
  40. // If there's no clear time set
  41. if(result.time.clear == 0 && result.time.attack === false) {
  42. attackString = result.time.reason ? 'NPC attacking will resume after '+result.time.reason : 'No attack currently set.';
  43. } else {
  44. // Build the string for the attack order
  45. $.each(result.order, function(key, value) {
  46. if(result.npcs[value].next){
  47. // If there's an attack happening right now, cross out NPCs that are in the hospital
  48. if(result.time.attack === true) {
  49. if(result.npcs[value].hosp_out >= result.time.current) {
  50. attackOrder += '<span style="text-decoration: line-through">'+result.npcs[value].name+'</span>, ';
  51. } else {
  52. attackOrder += result.npcs[value].name+', ';
  53. }
  54. } else {
  55. attackOrder += result.npcs[value].name+', ';
  56. }
  57. }
  58. // Adjust the current target based on if an attack is going and who isn't in the hospital yet
  59. if(result.time.attack === true) {
  60. if(result.npcs[value].hosp_out <= result.time.current) { // Check if the NPC is currently out of the hospital
  61. if(attackTarget == 0) {
  62. attackTarget = value;
  63. }
  64. }
  65. }
  66. });
  67.  
  68. // Check if target has been set, otherwise default to first in attack order
  69. if(attackTarget == 0) {
  70. attackTarget = result.order[0];
  71. }
  72.  
  73. // Clean up the attack order string
  74. attackOrder = attackOrder.slice(0, -2)+'.';
  75.  
  76. // Check if an attack is currently happening and adjust the message accordingly
  77. if(result.time.attack === true) {
  78. attackString = 'NPC attack is underway! Get in there and get some loot!';
  79. attackLink = 'loader.php?sid=attack&user2ID='+attackTarget;
  80. } else {
  81. attackString = 'NPC attack set for '+utcformat(result.time.clear)+'. Order is: '+attackOrder;
  82. attackLink = 'loader.php?sid=attack&user2ID='+attackTarget;
  83. }
  84. }
  85.  
  86. // Insert the custom news item into the news ticker
  87. let attackItem = {ID: 0, headline: '<span style="color:'+color+'; font-weight: bold;" id="icey-npctimer">'+attackString+'</span>', countdown: true, endTime: result.time.clear, link: attackLink, isGlobal: true, type: 'generalMessage'};
  88. data.headlines.unshift(attackItem);
  89. }, function(err) {
  90. console.log(err); // Error: "It broke"
  91. });
  92.  
  93. return data
  94. })
  95.  
  96. response.json = json;
  97. response.text = async () =>JSON.stringify(await json());
  98.  
  99. return response;
  100. };
  101.  
  102. function modifyContent() {
  103. return new Promise((resolve, reject) => {
  104. var ticker = document.querySelector('.news-ticker-countdown');
  105. ticker.style.color = color;
  106. var wrap = ticker.parentNode.parentNode.parentNode;
  107. var svg = wrap.children[0];
  108. svg.setAttribute('fill', color);
  109. svg.setAttribute('viewBox', "0 0 24 24");
  110. svg.setAttribute('height', '14');
  111. svg.setAttribute('width', '14');
  112. svg.children[0].setAttribute('d', 'M17.457 3L21 3.003l.002 3.523-5.467 5.466 2.828 2.829 1.415-1.414 1.414 1.414-2.474 2.475 2.828 2.829-1.414 1.414-2.829-2.829-2.475 2.475-1.414-1.414 1.414-1.415-2.829-2.828-2.828 2.828 1.415 1.415-1.414 1.414-2.475-2.475-2.829 2.829-1.414-1.414 2.829-2.83-2.475-2.474 1.414-1.414 1.414 1.413 2.827-2.828-5.46-5.46L3 3l3.546.003 5.453 5.454L17.457 3zm-7.58 10.406L7.05 16.234l.708.707 2.827-2.828-.707-.707zm9.124-8.405h-.717l-4.87 4.869.706.707 4.881-4.879v-.697zm-14 0v.7l11.241 11.241.707-.707L5.716 5.002l-.715-.001z');
  113. // console.log(svg);
  114. resolve('Content updated');
  115. });
  116. }
  117.  
  118. const newstickerObserver = new MutationObserver((mutationsList, observer) => {
  119. if ($(".news-ticker-slide #icey-npctimer").length == 1) { // If it's showing the slide for NPCs
  120. // Once changes are observed, disconnect the observer to avoid infinite loop
  121. newstickerObserver.disconnect();
  122.  
  123. // Modify the content of .news-ticker-wrapper
  124. modifyContent()
  125. .then(() => {
  126. // Re-observe the element after modifications and asynchronous operations are complete
  127. startNewstickerObserver();
  128. })
  129. .catch(error => console.error('Error updating content:', error));
  130. }
  131. });
  132.  
  133. function startNewstickerObserver() {
  134. const target = document.querySelector('.news-ticker-slider-wrapper');
  135. if (target) {
  136. newstickerObserver.observe(target, {
  137. childList: true, // Set true if children of the target node are being added or removed.
  138. attributes: false, // Set true if attributes of the target node are being modified.
  139. subtree: true, // Set true if changes to descendants of the target node are to be observed.
  140. characterData: false // Set true if data of the target node itself is being modified.
  141. });
  142. }
  143. }
  144.  
  145. /******************** HELPER FUNCTIONS ********************/
  146.  
  147. function waitForElm(selector) {
  148. return new Promise(resolve => {
  149. if (document.querySelector(selector)) {
  150. return resolve(document.querySelector(selector));
  151. }
  152.  
  153. const observer = new MutationObserver(mutations => {
  154. if (document.querySelector(selector)) {
  155. observer.disconnect();
  156. resolve(document.querySelector(selector));
  157. }
  158. });
  159.  
  160. // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
  161. observer.observe(document.body, {
  162. childList: true,
  163. subtree: true
  164. });
  165. });
  166. }
  167.  
  168. // Make sure the news ticker with the injected div is loaded
  169. waitForElm('#icey-npctimer').then((elm) => {
  170. startNewstickerObserver();
  171. //console.log('Element is ready');
  172. });
  173.  
  174. // Format the time in the appropriate fashion
  175. function utcformat(d){
  176. d= new Date(d * 1000);
  177. if(local) {
  178. var tail= ' LT', D= [d.getFullYear(), d.getMonth()+1, d.getDate()],
  179. T= [d.getHours(), d.getMinutes(), d.getSeconds()];
  180. } else {
  181. var tail= ' TCT', D= [d.getUTCFullYear(), d.getUTCMonth()+1, d.getUTCDate()],
  182. T= [d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds()];
  183. }
  184. if(format == 12) {
  185. /* 12 hour format */
  186. if(+T[0]> 12){
  187. T[0]-= 12;
  188. tail= 'PM '+tail;
  189. }
  190. else tail= 'AM '+tail;
  191. }
  192. var i= 3;
  193. while(i){
  194. --i;
  195. if(D[i]<10) D[i]= '0'+D[i];
  196. if(T[i]<10) T[i]= '0'+T[i];
  197. }
  198. return T.join(':')+ tail;
  199. }
  200.  
  201. // Fetch the NPC details from Loot Rangers
  202. async function getAttackTimes() {
  203. return new Promise(resolve => {
  204. const request_url = `https://api.lzpt.io/loot`;
  205. GM_xmlhttpRequest ({
  206. method: "GET",
  207. url: request_url,
  208. headers: {
  209. "Content-Type": "application/json"
  210. },
  211. onload: response => {
  212. try {
  213. const data = JSON.parse(response.responseText);
  214. if(!data) {
  215. console.log('No response from Loot Rangers');
  216. } else {
  217. return resolve(data)
  218. }
  219. }
  220. catch (e) {
  221. console.error(e);
  222. }
  223.  
  224. },
  225. onerror: (e) => {
  226. console.error(e);
  227. }
  228. })
  229. });
  230. }