diff --git a/npc_loot_timers.user.js b/npc_loot_timers.user.js
new file mode 100644
index 0000000..2a33b52
--- /dev/null
+++ b/npc_loot_timers.user.js
@@ -0,0 +1,453 @@
+// ==UserScript==
+// @name Torn: Loot Timers
+// @namespace lugburz.show_timer_on_npc_profile
+// @version 1.0
+// @description Displays NPC loot data on profiles, the sidebar, and at the top of the page.
+// @author Lugburz, Lazerpent
+// @match https://www.torn.com/*
+// @updateURL https://github.com/f2404/torn-userscripts/raw/master/npc_profile_loot_timer.user.js
+// @downloadURL https://github.com/f2404/torn-userscripts/raw/master/npc_profile_loot_timer.user.js
+// @connect api.lzpt.io
+// @grant GM_xmlhttpRequest
+// @grant GM_addStyle
+// @grant GM_setValue
+// @grant GM_getValue
+// ==/UserScript==
+
+/*
+ Below are the configuration options. Change these to match your preferences. All options default true
+
+ SIDEBAR_TIMERS: If you want to see the timers in the sidebar
+ TOPBAR_TIMERS: If you want to see the timers in the topbar
+ CHANGE_COLOR: If you want the timer to change color as it gets close to zero
+ */
+
+const SIDEBAR_TIMERS = true;
+const TOPBAR_TIMERS = true;
+const CHANGE_COLOR = true;
+
+// --- END CONFIGURATION --- //
+
+GM_addStyle(`
+ .npc_orange-timer {
+ color: orange;
+ }
+
+ .npc_red-timer {
+ color: red;
+ }
+
+ .npc_green-timer {
+ color: green;
+ }
+
+ .npc_show-hide {
+ color: #069;
+ text-decoration: none;
+ cursor: pointer;
+ float: right;
+ -webkit-transition: color .2s ease;
+ -o-transition: color .2s ease;
+ transition: color .2s ease;
+ }
+
+ .npc_link {
+ display: inline-block;
+ text-decoration-color: black !important;
+ text-decoration-thickness: 2px !important;
+ }
+`);
+
+const ROMAN = [null, 'I', 'II', 'III', 'IV', 'V'];
+const TIMINGS = [-1, 0, 30, 90, 210, 450].map(i => i * 60);
+const SHORT_NAME = name => {
+ return {
+ 'Fernando': 'Nando', 'Easter Bunny': 'Bunny'
+ }[name] || name;
+};
+
+const LOGGING_ENABLED = false;
+
+function log(data) {
+ if (LOGGING_ENABLED) console.log(data)
+}
+
+async function getData() {
+ try {
+ const str_data = GM_getValue('cached_data');
+ const last_updated = GM_getValue('last_updated');
+
+ const data = JSON.parse(str_data);
+
+ const now = new Date().getTime();
+
+ const noClear = data.time.clear === 0 && !data.time.reason;
+ const pastClear = now / 1000 > data.time.clear;
+ const overOneMinute = now - last_updated > 60 * 1000;
+ const overFifteenMinutes = now - last_updated >= 15 * 60 * 1000;
+
+ const shouldRequest = overOneMinute && (noClear || pastClear) || overFifteenMinutes;
+ if (!shouldRequest) {
+ return data;
+ }
+ } catch (e) {
+ }
+
+ log('Calling API');
+
+ return await new Promise((resolve, reject) => {
+ GM_xmlhttpRequest({
+ method: 'GET', url: 'https://api.lzpt.io/loot/', headers: {
+ 'Content-Type': 'application/json'
+ }, onload: (response) => {
+ try {
+ let data = JSON.parse(response.responseText);
+ GM_setValue('cached_data', JSON.stringify(data));
+ GM_setValue('last_updated', new Date().getTime());
+
+ resolve(data);
+ } catch (err) {
+ GM_setValue('last_updated', 0);
+ reject(err);
+ }
+ }, onerror: (err) => {
+ GM_setValue('last_updated', 0);
+ reject(err);
+ }
+ });
+ });
+}
+
+function hideTimers(hide, sidebar) {
+ const bar = $(`#npc${sidebar ? 'Side' : 'Top'}barData`);
+ hide ? bar.hide() : bar.show();
+ $(`#showHide${sidebar ? 'Side' : 'Top'}barTimers`).text(`[${hide ? sidebar ? 'show' : 'snow NPC timers' : 'hide'}]`);
+}
+
+function maybeChangeColors(span, time, level = 3, active = true) {
+ let color = '';
+ if (level < 4) {
+ if (time < 5 * 60) {
+ color = 'red';
+ } else if (time < 10 * 60) {
+ color = 'orange';
+ }
+ } else if (level >= 4) {
+ color = 'green';
+ }
+
+ if (CHANGE_COLOR && active && color) {
+ $(span).addClass(`npc_${color}-timer`);
+ } else {
+ $(span).removeClass('npc_orange-timer npc_red-timer npc_green-timer');
+ }
+}
+
+function process(id, data) {
+ if (!data.npcs[id]) {
+ return;
+ }
+
+ let x = setInterval(function () {
+ const now = Math.floor(new Date().getTime() / 1000);
+ const elapsedTime = now - data.npcs[id].hosp_out;
+ const currentLevel = elapsedTime < TIMINGS[5] ? TIMINGS.findIndex(t => elapsedTime < t) - 1 : 5;
+ const remaining = elapsedTime < TIMINGS[4] ? TIMINGS[4] - elapsedTime : TIMINGS[5] - elapsedTime;
+
+ if (remaining < 0) {
+ clearInterval(x);
+ return;
+ }
+
+ const status = $('div.profile-status').find('div.profile-container').find('div.description');
+ const subDesc = status.find('span.sub-desc');
+ let subHtml = $(subDesc).html();
+
+ if (subHtml) {
+ const n = subHtml.indexOf('(');
+ subHtml = subHtml.substring(0, n !== -1 ? n - 1 : subHtml.length);
+
+ let location = 0;
+ for (const l of data.order) {
+ if (data.npcs[l].next) {
+ location++;
+ }
+ if (l === id) {
+ break;
+ }
+ }
+ if (currentLevel !== 5) {
+ subDesc.html(`${subHtml} (Loot Level ${ROMAN[currentLevel === 4 ? 5 : 4]} in ${formatCountdown(remaining)})`);
+ }
+
+ if (data.time.clear > now && data.npcs[id].next) {
+ subDesc.attr('title', `Attack ${location} at ${formatTornTime(data.time.clear)}`);
+ } else if (data.time.attack) {
+ subDesc.attr('title', 'Attack Right Now');
+ } else if (data.time.reason) {
+ subDesc.attr('title', "Attack resumes after " + data.time.reason);
+ } else {
+ subDesc.attr('title', `Attack Not Scheduled`);
+ }
+ }
+ }, 1000);
+}
+
+const newsContainerId = 'header-swiper-container';
+const lightTextColor = '#aaa';
+const lightLinkColor = '#00a9f8';
+const darkTextColor = 'var(--default-color)';
+const darkLinkColor = 'var(--default-blue-color)';
+
+const isMobile = () => $('#tcLogo').height() < 50;
+const addContentPadding = (add) => $('#mainContainer > div.content-wrapper').css('padding-top', add ? '10px' : '0px');
+// noinspection JSJQueryEfficiency
+const setTopbarPadding = (pad) => $('#topbarNpcTimers').css('padding-top', pad);
+
+function addNpcTimers(data) {
+ if (!data) {
+ return;
+ }
+
+ log('Adding NPC Timers')
+ if (SIDEBAR_TIMERS && $('#sidebarNpcTimers').size() < 1) {
+ const npc_html = data.order.map(id => [id, data.npcs[id].name]).map(([id, name]) => (`
+
+ `)).join('');
+
+ const div = `
+
+
+ `;
+
+
+ $('#sidebar').find('div[class^=toggle-content__]').find('div[class^=content___]').append(div);
+
+ $('#showHideSidebarTimers').on('click', function () {
+ const hide = $('#showHideSidebarTimers').text() === '[hide]';
+ GM_setValue('hideSidebarTimers', hide);
+ hideTimers(hide, true);
+ });
+ }
+
+ // noinspection JSJQueryEfficiency
+ if (TOPBAR_TIMERS && $('#topbarNpcTimers').size() < 1) {
+ const npc_html = data.order.map(id => [id, data.npcs[id].name]).map(([id, name]) => (`
+
+ ${SHORT_NAME(name)}:
+
+
+ `)).join('');
+
+ const div = `
+
+
+ [hide]
+
+
+
+
+
+
+ ${npc_html}
+
+
+ `;
+
+ $('div.header-wrapper-bottom').find('div.container').append(div);
+ const isNewsTickerDisplayed = $(`#${newsContainerId}`).size() > 0;
+ const topbarTimers = $('#topbarNpcTimers');
+ topbarTimers.css('color', isNewsTickerDisplayed ? darkTextColor : lightTextColor);
+ topbarTimers.find('a').css('color', isNewsTickerDisplayed ? darkLinkColor : lightLinkColor);
+ addContentPadding(isNewsTickerDisplayed);
+
+ $('#showHideTopbarTimers').on('click', function () {
+ const hide = $('#showHideTopbarTimers').text() === '[hide]';
+ GM_setValue('hideTopbarTimers', hide);
+ hideTimers(hide, false);
+ });
+
+ topbarTimers.find('span').first().css('padding-left', isMobile() ? '4px' : '190px');
+ }
+
+ if (SIDEBAR_TIMERS) {
+ const hide = GM_getValue('hideSidebarTimers');
+ hideTimers(hide, true);
+ }
+ if (TOPBAR_TIMERS) {
+ const hide = GM_getValue('hideTopbarTimers');
+ hideTimers(hide, false);
+ }
+
+ if (!setup) {
+ setup = true;
+ renderTimes().catch(err => {
+ console.error(err);
+ });
+ }
+}
+
+let setup = false;
+
+async function renderTimes() {
+ const start = new Date().getTime();
+ const data = await getData();
+ const now = Math.floor(start / 1000);
+
+ data.order.forEach(id => {
+ const sidebar = `#npcTimerSidebar${id}`;
+ const topbar = `#npcTimerTopbar${id}`;
+
+ const elapsedTime = now - data.npcs[id].hosp_out;
+ const remaining = elapsedTime < TIMINGS[4] ? TIMINGS[4] - elapsedTime : TIMINGS[5] - elapsedTime;
+ const currentLevel = elapsedTime < TIMINGS[5] ? TIMINGS.findIndex(t => elapsedTime < t) - 1 : 5;
+
+ const clearStatus = data.npcs[id].next ? 'none' : 'line-through';
+
+ if (SIDEBAR_TIMERS) {
+ const div = $(sidebar);
+ const span = div.find('span');
+ let text;
+ if (currentLevel >= 4) {
+ text = elapsedTime < 0 ? 'Hosp' : `Loot level ${ROMAN[currentLevel]}`;
+ } else {
+ text = formatCountdown(remaining);
+ }
+ $(span).text(text);
+ maybeChangeColors(span, remaining, currentLevel);
+
+ div.find('a').first().css('text-decoration', clearStatus);
+ }
+ if (TOPBAR_TIMERS) {
+ const div = $(topbar);
+ const span = div.find('span');
+ let text;
+ if (currentLevel >= 4) {
+ text = elapsedTime < 0 ? 'Hosp' : (isMobile() ? `LL ${ROMAN[currentLevel]}` : `Loot lvl ${ROMAN[currentLevel]}`);
+ } else {
+ text = isMobile() ? formatCountdown(remaining, 'minimal') : formatCountdown(remaining, 'short');
+ }
+ $(span).text(text);
+ maybeChangeColors(span, remaining, currentLevel);
+
+ div.find('a').first().css('text-decoration', clearStatus);
+ }
+ });
+
+ const remainingTime = data.time.clear - now;
+ const scheduled = remainingTime > 0;
+
+ if (SIDEBAR_TIMERS) {
+ const sidebar = $('#npcTimerSidebarScheduledAttack');
+ const span = sidebar.find('span');
+ const text = scheduled ? formatCountdown(remainingTime) : data.time.attack ? 'NOW' : data.time.reason ? `${data.time.reason}` :'N/A';
+ sidebar.find('span').text(text);
+
+ maybeChangeColors(span, remainingTime, undefined, scheduled || data.time.attack);
+
+ sidebar.attr('title', scheduled ? `Attack scheduled for ${formatTornTime(data.time.clear)}` : data.time.attack ? 'Attack now' : data.time.reason ? `Attacking resumes after ${data.time.reason}` :'No attack scheduled');
+ }
+ if (TOPBAR_TIMERS) {
+ const topbar = $('#npcTimerTopbarScheduledAttack');
+ const span = topbar.find('span');
+ const text = scheduled ? (isMobile() ? formatCountdown(remainingTime, 'minimal') : formatCountdown(remainingTime, 'short')) : data.time.attack ? 'NOW' : data.time.reason ? `On Hold` :'N/A';
+ topbar.find('span').text(text);
+
+ maybeChangeColors(span, remainingTime, undefined, scheduled || data.time.attack);
+
+ topbar.attr('title', scheduled ? `Attack scheduled for ${formatTornTime(data.time.clear)}` : data.time.attack ? 'Attack now' : data.time.reason ? `Attacking resumes after ${data.time.reason}` : 'No attack scheduled');
+ }
+
+ setTimeout(renderTimes, Math.max(100, start + 1000 - new Date().getTime()));
+}
+
+
+(function () {
+ 'use strict';
+
+ if ($(location).attr('href').includes('profiles.php')) {
+ try {
+ const profileId = RegExp(/XID=(\d+)/).exec($(location).attr('href'))[1];
+ getData().then(process.bind(null, parseInt(profileId)));
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ const maybeAddTimers = () => {
+ if (SIDEBAR_TIMERS && $('#sidebar').size() > 0 || TOPBAR_TIMERS && $('div.header-wrapper-bottom').size() > 0) {
+ getData().then(data => addNpcTimers(data));
+ }
+ };
+ maybeAddTimers();
+
+ // try again to handle new tab case
+ setTimeout(maybeAddTimers, 1000);
+})();
+
+// News ticker observer
+const observer = new MutationObserver(function (mutations) {
+ mutations.forEach(function (mutation) {
+ const topbarNpcTimers = $('#topbarNpcTimers');
+ for (const node of mutation.addedNodes) {
+ if ($(node).attr('id') === newsContainerId) {
+ topbarNpcTimers.css('color', darkTextColor);
+ topbarNpcTimers.find('a').css('color', darkLinkColor);
+ addContentPadding(true);
+
+ setTopbarPadding(isMobile() ? $('#sidebarroot').height() : 0);
+ }
+ }
+ for (const node of mutation.removedNodes) {
+ if ($(node).attr('id') === newsContainerId) {
+ topbarNpcTimers.css('color', lightTextColor);
+ topbarNpcTimers.find('a').css('color', lightLinkColor);
+ addContentPadding(false);
+ setTopbarPadding(0);
+ }
+ }
+ });
+});
+
+observer.observe(document.getElementById('header-root'), {
+ subtree: true, childList: true, characterData: false, attributes: false, attributeOldValue: false
+});
+
+
+// Helper Functions
+
+function formatTornTime(time) {
+ const date = new Date(time * 1000);
+ return `${pad(date.getUTCHours(), 2)}:${pad(date.getUTCMinutes(), 2)}:${pad(date.getUTCSeconds(), 2)} - ${pad(date.getUTCDate(), 2)}/${pad(date.getUTCMonth() + 1, 2)}/${date.getUTCFullYear()} TCT`;
+}
+
+function formatCountdown(sec, mode = 'long') {
+ const hours = Math.floor((sec % (60 * 60 * 24)) / (60 * 60));
+ const minutes = Math.floor((sec % (60 * 60)) / 60);
+ const seconds = Math.floor(sec % 60);
+
+ switch (mode) {
+ case 'long':
+ return (hours > 0 ? hours + 'h ' : '') + (hours > 0 || minutes > 0 ? minutes + 'min ' : '') + seconds + 's';
+ case 'short':
+ return hours > 0 ? `${hours}h ${minutes}m` : minutes > 0 ? `${minutes}min ${seconds}s` : `${seconds}s`;
+ case 'minimal':
+ return (hours > 0 ? hours + ':' : '') + (hours > 0 || minutes > 0 ? pad(minutes, 2) + ':' : '') + pad(seconds, 2);
+ }
+}
+
+function pad(num, size) {
+ return String(num).padStart(size, '0');
+}
\ No newline at end of file
diff --git a/npc_profile_loot_timer.user.js b/npc_profile_loot_timer.user.js
deleted file mode 100644
index 4e13640..0000000
--- a/npc_profile_loot_timer.user.js
+++ /dev/null
@@ -1,511 +0,0 @@
-// ==UserScript==
-// @name Torn: Loot timer on NPC profile
-// @namespace lugburz.show_timer_on_npc_profile
-// @version 0.3.3
-// @description Add a countdown timer to desired loot level on the NPC profile page as well as in the sidebar and the topbar (optionally).
-// @author Lugburz
-// @match https://www.torn.com/*
-// @require https://raw.githubusercontent.com/f2404/torn-userscripts/d8fb88fbc7e03173aa81b1b466b1d2a251a70aad/lib/lugburz_lib.js
-// @updateURL https://github.com/f2404/torn-userscripts/raw/master/npc_profile_loot_timer.user.js
-// @downloadURL https://github.com/f2404/torn-userscripts/raw/master/npc_profile_loot_timer.user.js
-// @connect yata.yt
-// @connect api.lzpt.io
-// @grant GM_xmlhttpRequest
-// @grant GM_addStyle
-// @grant GM_setValue
-// @grant GM_getValue
-// ==/UserScript==
-
-// Whether or not to show timer in sidebar
-// true by default
-const SIDEBAR_TIMERS = true;
-
-// Whether or not to show timer in topbar
-//true by default
-const TOPBAR_TIMERS = true;
-
-// Whether or not to show scheduled attack timer provided by the Loot Rangers discord API
-// true by default
-const ATTACK_TIMER = true;
-
-// Whether or not to change the timer color when it's close to running out (true by default)
-const CHANGE_COLOR = true;
-
-// The NPC's to watch. Remove any that you don't want
-// Format: 'NPC_name': { id: NPC_id, loot_level: desired_loot_level_for_this_NPC }
-const NPCS = {
- 'Duke': { id: 4, loot_level: 4 },
- 'Scrooge': { id: 10, loot_level: 4 },
- 'Leslie': { id: 15, loot_level: 4 },
- 'Jimmy': { id: 19, loot_level: 4 },
- 'Nando': { id: 20, loot_level: 4 },
- 'Tiny': { id: 21, loot_level: 4 },
- 'Bunny': { id: 17, loot_level: 4 }
-};
-
-
-GM_addStyle(`
-.timers-div {
- background: #f2f2f2;
- line-height: 16px;
- padding: 8px 10px 0;
- margin: 1px 0;
- border-bottom-right-radius: 5px;
- border-top-right-radius: 5px;
- cursor: default;
- overflow: hidden;
- border-bottom: 1px solid #fff;
-}
-.orange-timer {
- color: orange;
-}
-.red-timer {
- color: red;
-}
-.show-hide {
- color: #069;
- text-decoration: none;
- cursor: pointer;
- float: right;
- -webkit-transition: color .2s ease;
- -o-transition: color .2s ease;
- transition: color .2s ease;
-}`);
-
-const ROMAN = ['I', 'II', 'III', 'IV', 'V'];
-const TIMINGS = [0, 30*60, 90*60, 210*60, 450*60]; // till loot levels
-const LOGGING_ENABLED = false;
-
-const YATA_API_URL = 'https://yata.yt/api/v1/loot/';
-const ATTACK_TIMER_API_URL = 'https://api.lzpt.io/loot/';
-
-const call_api = async (url) => {
- return new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: 'GET',
- url,
- headers: {
- 'Content-Type': 'application/json'
- },
- onload: (response) => {
- try {
- const resjson = JSON.parse(response.responseText);
- resolve(resjson);
- } catch (err) {
- reject(err);
- }
- },
- onerror: (err) => {
- reject(err);
- }
- });
- });
-}
-
-/* Loot Timers */
-
-function getLootLevel(id) {
- let loot_level = 0;
- Object.values(NPCS).forEach(npc => {
- if (npc.id == id) {
- loot_level = npc.loot_level;
- }
- });
- return loot_level;
-}
-
-function getDesiredLootLevelTs(data, id) {
- if (!data.hosp_out[id]) return -1;
- const loot_level = getLootLevel(id);
- return data.hosp_out[id] + TIMINGS[loot_level - 1];
-}
-
-function isCachedDataValid(id = '') {
- const str_data = GM_getValue('cached_data');
- let data = '';
- try {
- data = JSON.parse(str_data);
- } catch (e) {
- return false;
- }
-
- if (!data.next_update) {
- return false;
- }
-
- const now = new Date().getTime();
- const last_updated = GM_getValue('last_updated');
- // do not call the API too often
- if ((now - last_updated < 10*60*1000) && (Math.floor(now / 1000) < data.next_update)) {
- return true;
- }
-
- if (id) {
- const loot_level = getLootLevel(id);
- if (!loot_level || !data.hosp_out[id] || getDesiredLootLevelTs(data, id) * 1000 < now) {
- return false;
- }
- } else {
- for (let id of Object.keys(data)) {
- const loot_level = getLootLevel(id);
- if (!loot_level || getDesiredLootLevelTs(data, id) * 1000 < now) {
- return false;
- }
- }
- }
- return true;
-}
-
-async function getTimings(id) {
- if (!isCachedDataValid(id)) {
- log('Calling the API id=' + id);
- const data = await call_api(YATA_API_URL);
- GM_setValue('cached_data', JSON.stringify(data));
- GM_setValue('last_updated', new Date().getTime());
- }
-
- const cached_data = JSON.parse(GM_getValue('cached_data'));
- if (cached_data.error) {
- console.error(`YATA API error: code=${cached_data.error.code} error=${cached_data.error.error}`);
- return -1;
- }
- // no data on the id
- if (!cached_data.hosp_out[id]) {
- return -1;
- }
- // timestamp of desired loot level
- return getDesiredLootLevelTs(cached_data, id);
-}
-
-async function getAllTimings() {
- if (!isCachedDataValid()) {
- log('Calling the API');
- const data = await call_api(YATA_API_URL);
- GM_setValue('cached_data', JSON.stringify(data));
- GM_setValue('last_updated', new Date().getTime());
- }
-
- const cached_data = JSON.parse(GM_getValue('cached_data'));
- if (cached_data.error) {
- console.error(`YATA API error: code=${cached_data.error.code} error=${cached_data.error.error}`);
- return '';
- }
- return cached_data;
-}
-
-function hideTimers(hide, yataData, sidebar = true) {
- log(yataData);
- if (sidebar) {
- Object.values(NPCS).forEach(npc => (hide || yataData.hosp_out[npc.id] === undefined) ? $(`#npcTimer${npc.id}`).hide() : $(`#npcTimer${npc.id}`).show());
- hide ? $('#npcTimerSideScheduledAttack').hide() : $('#npcTimerSideScheduledAttack').show();
- $('#showHideTimers').text(`[${hide ? 'show' : 'hide'}]`);
- } else {
- Object.values(NPCS).forEach(npc => (hide || yataData.hosp_out[npc.id] === undefined) ? $(`#npcTimerTop${npc.id}`).hide() : $(`#npcTimerTop${npc.id}`).show());
- hide ? $('#npcTimerTopScheduledAttack').hide() : $('#npcTimerTopScheduledAttack').show();
- $('#showHideTopbarTimers').html(`[${hide ? 'show NPC timers' : 'hide'}]`);
- }
-}
-
-function maybeChangeColors(span, left) {
- if (CHANGE_COLOR) {
- if (left < 5 * 60 * 1000) { // 5 minutes
- $(span).addClass('red-timer');
- } else if (left < 10 * 60 * 1000) { // 10 minutes
- $(span).addClass('orange-timer');
- } else {
- $(span).removeClass('orange-timer');
- $(span).removeClass('red-timer');
- }
- }
-}
-
-function formatTimeSec(msec) {
- return formatTimeMsec(msec).replace(/\..+/, '');
-}
-
-function process(ts, loot_level) {
- if (ts < 0) {
- return;
- }
-
- // ts is s, Date is ms
- const due = new Date(ts * 1000);
-
- let x = setInterval(function () {
- const now = new Date().getTime();
- const left = due - now;
- if (left < 0) {
- clearInterval(x);
- return;
- }
-
- // Display the result
- const span = $('div.profile-status').find('div.profile-container').find('div.description').find('span.sub-desc');
- let html = $(span).html();
- if (html) {
- const n = html.indexOf('(');
- html = html.substring(0, n != -1 ? n - 1 : html.length);
- $(span).html(html + " (Till loot level " + ROMAN[loot_level - 1] + ": " + formatTimeSecWithLetters(left) + ")");
- }
- }, 1000);
-}
-
-const newsContainerId = 'header-swiper-container';
-const lightTextColor = '#aaa';
-const lightLinkColor = '#00a9f8';
-const darkTextColor = 'var(--default-color)';
-const darkLinkColor = 'var(--default-blue-color)';
-
-const isMobile = () => $('#tcLogo').height() < 50;
-const addContentPadding = (add) => $('#mainContainer > div.content-wrapper').css('padding-top', add ? '10px' : '0px');
-const setTopbarPadding = (pad) => $('#topbarNpcTimers').css('padding-top', pad);
-
-function addNpcTimers(data) {
- if (!data)
- return;
-
- const getLl = (elapsed => (elapsed < TIMINGS[TIMINGS.length - 1]) ? ROMAN[TIMINGS.findIndex(t => elapsed < t) - 1] : ROMAN[ROMAN.length - 1]);
-
- log('Adding NPC Timers for:')
- log(NPCS);
- if (SIDEBAR_TIMERS && $('#sidebarNpcTimers').size() < 1) {
- let div = '
';
- $('#sidebar').find('div[class^=toggle-content__]').find('div[class^=content___]').append(div);
- //$(div).insertBefore($('#sidebar').find('h2[class^=header__]').eq(1)); // second header
- $('#showHideTimers').on('click', function () {
- const hide = $('#showHideTimers').text() == '[hide]';
- GM_setValue('hideSidebarTimers', hide);
- hideTimers(hide, data);
- });
- }
-
- if (TOPBAR_TIMERS && $('#topbarNpcTimers').size() < 1) {
- let div = '' +
- '[hide]';
-
- if (ATTACK_TIMER) {
- const pistolImg = '

';
- div += `
${pistolImg} `;
- }
-
- Object.keys(NPCS).forEach(name => {
- div += `
${name}: `;
- });
-
- div += '
';
-
- if ($('div.header-wrapper-bottom').find('div.container').size() > 0) {
- // announcement
- $('div.header-wrapper-bottom').find('div.container').append(div);
- const isNewsTickerDisplayed = $(`#${newsContainerId}`).size() > 0;
- $('#topbarNpcTimers').css('color', isNewsTickerDisplayed ? darkTextColor : lightTextColor);
- $('#topbarNpcTimers').find('a').css('color', isNewsTickerDisplayed ? darkLinkColor : lightLinkColor);
- addContentPadding(isNewsTickerDisplayed);
- } else {
- $('div.header-wrapper-bottom').prepend(div);
- $('#topbarNpcTimers').find('a').css('color', '#069');
- }
- $('#showHideTopbarTimers').on('click', function () {
- const hide = $('#showHideTopbarTimers').text() == '[hide]';
- GM_setValue('hideTopbarTimers', hide);
- hideTimers(hide, data, false);
- });
- // phone or desktop mode
- $('#topbarNpcTimers').find('span').first().css('padding-left', isMobile() ? '4px' : '190px');
- }
-
- if (SIDEBAR_TIMERS) {
- const hide = GM_getValue('hideSidebarTimers');
- hideTimers(hide, data);
- }
- if (TOPBAR_TIMERS) {
- const hide = GM_getValue('hideTopbarTimers');
- hideTimers(hide, data, false);
- }
-
- Object.keys(NPCS).forEach(name => {
- const id = NPCS[name].id;
- const loot_level = NPCS[name].loot_level;
- const pId = '#npcTimer' + id;
- const spanId = '#npcTimerTop' + id;
- if (data.hosp_out[id]) {
- const ts = getDesiredLootLevelTs(data, id);
-
- // ts is s, Date is ms
- const due = new Date(ts * 1000);
- let x = setInterval(function () {
- const now = new Date().getTime();
- const left = due - now;
-
- // Display the results
- if (SIDEBAR_TIMERS) {
- $(pId).attr('notavail', '');
- const span = $(pId).find('span');
- let text;
- if (left < 0) {
- const elapsed = Math.floor(now / 1000) - data.hosp_out[id];
- text = elapsed < 0 ? 'Hosp' : `Loot level ${getLl(elapsed)}`;
- } else {
- text = formatTimeSecWithLetters(left);
- }
- $(span).text(text);
- maybeChangeColors(span, left);
- }
- if (TOPBAR_TIMERS) {
- $(spanId).attr('notavail', '');
- const span = $(spanId).find('span');
- let text;
- if (left < 0) {
- const elapsed = Math.floor(now / 1000) - data.hosp_out[id];
- text = elapsed < 0 ? 'Hosp' : (isMobile() ? `LL ${getLl(elapsed)}` : `Loot lvl ${getLl(elapsed)}`);
- } else {
- text = isMobile() ? formatTimeSec(left) : formatTimeSecWithLettersShort(left);
- }
- $(span).text(text);
- maybeChangeColors(span, left);
- }
-
- if (left < 0) {
- clearInterval(x);
- }
- }, 1000);
- } else {
- $(pId).attr('notavail', 1);
- $(pId).hide();
- $(spanId).attr('notavail', 1);
- $(spanId).hide();
- }
- })
-}
-
-/* Attack Timer */
-
-const ATTACK_TIMER_API_DELAY = 60 * 1000;
-let displayAttackTimer = -1;
-
-function formatTimeTorn(ts) {
- const date = new Date(ts);
- return `${pad(date.getUTCHours(), 2)}:${pad(date.getUTCMinutes(), 2)}:${pad(date.getUTCSeconds(), 2)} - ${pad(date.getUTCDate(), 2)}/${pad(date.getUTCMonth() + 1, 2)}/${date.getUTCFullYear()} TCT`;
-}
-
-async function getAttackTime() {
- const data = await call_api(ATTACK_TIMER_API_URL);
- log(`getAttackTime: ${JSON.stringify(data)}`);
- const attackTs = data && data.time && data.time.clear ? data.time.clear * 1000 : 0;
- GM_setValue('attack_ts_cached', attackTs);
- GM_setValue('attack_ts_last_updated', new Date().getTime());
-}
-
-async function addScheduledAttackTimer() {
- const now = new Date().getTime();
- const lastUpdated = GM_getValue('attack_ts_last_updated');
- let attackTs = GM_getValue('attack_ts_cached');
-
- if (!attackTs || !lastUpdated || now - lastUpdated >= ATTACK_TIMER_API_DELAY) {
- log('Calling attack timer API');
- await getAttackTime();
- }
-
- setInterval(async () => {
- log('Calling attack timer API, timer');
- await getAttackTime();
- startDisplayingScheduledAttack();
- }, ATTACK_TIMER_API_DELAY);
-
- startDisplayingScheduledAttack();
-
- attackTs = GM_getValue('attack_ts_cached');
- $('#npcTimerTopScheduledAttack').find('img').attr('title', attackTs ? `Attack scheduled for ${formatTimeTorn(attackTs)}` : 'Attack scheduled');
- $('#npcTimerSideScheduledAttack').attr('title', attackTs ? `Attack scheduled for ${formatTimeTorn(attackTs)}` : '');
-}
-
-function startDisplayingScheduledAttack() {
- if (displayAttackTimer > -1) {
- clearInterval(displayAttackTimer);
- }
-
- displayAttackTimer = setInterval(() => {
- const attackTs = GM_getValue('attack_ts_cached');
- const due = new Date(attackTs);
- const now = new Date().getTime();
- const left = due - now;
- if (SIDEBAR_TIMERS) {
- const span = $('#npcTimerSideScheduledAttack').find('span');
- const text = left < 0 ? 'N/A' : formatTimeSecWithLetters(left);
- $('#npcTimerSideScheduledAttack').find('span').text(text);
- if (text !== 'N/A') maybeChangeColors(span, left);
- }
- if (TOPBAR_TIMERS) {
- const span = $('#npcTimerTopScheduledAttack').find('span');
- const text = left < 0 ? 'N/A' : (isMobile() ? formatTimeSec(left) : formatTimeSecWithLettersShort(left));
- $('#npcTimerTopScheduledAttack').find('span').text(text);
- if (text !== 'N/A') maybeChangeColors(span, left);
- }
- }, 1000);
-}
-
-
-
-(function () {
- 'use strict';
-
- // Your code here...
- if ($(location).attr('href').includes('profiles.php')) {
- const profileId = RegExp(/XID=(\d+)/).exec($(location).attr('href'))[1];
- Object.values(NPCS).forEach(npc => {
- if (npc.id == profileId) {
- getTimings(profileId).then(ts => process(ts, npc.loot_level));
- }
- });
- }
-
- const maybeAddTimers = () => {
- if (SIDEBAR_TIMERS && $('#sidebar').size() > 0 || TOPBAR_TIMERS && $('div.header-wrapper-bottom').size() > 0) {
- getAllTimings().then(data => addNpcTimers(data));
- if (ATTACK_TIMER) {
- addScheduledAttackTimer();
- }
- }
- };
- maybeAddTimers();
- // try again to handle new tab case
- setTimeout(maybeAddTimers, 1000);
-})();
-
-function log(data) {
- if (LOGGING_ENABLED) console.log(data)
-}
-
-// News ticker observer
-const observer = new MutationObserver(function(mutations) {
- mutations.forEach(function(mutation) {
- for (const node of mutation.addedNodes) {
- if ($(node).attr('id') === newsContainerId) {
- $('#topbarNpcTimers').css('color', darkTextColor);
- $('#topbarNpcTimers').find('a').css('color', darkLinkColor);
- addContentPadding(true);
- // move the topbar down in mobile mode if news ticker is enabled
- setTopbarPadding(isMobile() ? $('#sidebarroot').height() : 0);
- }
- }
- for (const node of mutation.removedNodes) {
- if ($(node).attr('id') === newsContainerId) {
- $('#topbarNpcTimers').css('color', lightTextColor);
- $('#topbarNpcTimers').find('a').css('color', lightLinkColor);
- addContentPadding(false);
- setTopbarPadding(0);
- }
- }
- });
-});
-
-observer.observe(document.getElementById('header-root'), { subtree: true, childList: true, characterData: false, attributes: false, attributeOldValue: false });