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]) => (` +

+ ${name} + +

+ `)).join(''); + + const div = ` +
+
+ NPC Timers + [hide] + +

+ Attack in +

+ ${npc_html} +
+
+ `; + + + $('#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] + + + + Attack scheduled + + + ${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 = '
NPC Timers[hide]'; - if (ATTACK_TIMER) { - div += '

Attack in

'; - } - Object.keys(NPCS).forEach(name => { - div += `

${name}

`; - }); - 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 = 'Attack scheduled'; - 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 });