From f3e0f7fef2eebcd9ff7dae9f06ea245b42dc7fcd Mon Sep 17 00:00:00 2001 From: Christopher Stoll Date: Sun, 18 Jun 2023 17:46:26 -0400 Subject: [PATCH 01/17] Script rewrite to fully integrate Loot Rangers --- npc_profile_loot_timer.user.js | 755 ++++++++++++++------------------- 1 file changed, 322 insertions(+), 433 deletions(-) diff --git a/npc_profile_loot_timer.user.js b/npc_profile_loot_timer.user.js index 4e13640..ff031a0 100644 --- a/npc_profile_loot_timer.user.js +++ b/npc_profile_loot_timer.user.js @@ -1,11 +1,9 @@ // ==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 +// @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/* -// @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 @@ -16,238 +14,144 @@ // @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; +/* + Below are the configuration options. Change these to match your preferences. All options default true -// Whether or not to show scheduled attack timer provided by the Loot Rangers discord API -// true by default -const ATTACK_TIMER = 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 + */ -// Whether or not to change the timer color when it's close to running out (true by default) +const SIDEBAR_TIMERS = true; +const TOPBAR_TIMERS = true; 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 } -}; - +// --- END CONFIGURATION --- // 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; -} + .npc_orange-timer { + color: orange; + } + + .npc_red-timer { + color: red; + } + + .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; +}; -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]; +const LOGGING_ENABLED = false; +function log(data) { + if (LOGGING_ENABLED) console.log(data) } -function isCachedDataValid(id = '') { +const updateData = async () => { + try { const str_data = GM_getValue('cached_data'); - let data = ''; - try { - data = JSON.parse(str_data); - } catch (e) { - return false; - } + const last_updated = GM_getValue('last_updated'); - if (!data.next_update) { - return false; - } + const data = JSON.parse(str_data); 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 (now - last_updated < 15 * 60 * 1000 && ((data.time.clear !== 0 && data.time.clear >= now / 1000) || now - last_updated <= 60 * 1000)) { + return data; } - - 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; - } + } 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) { + reject(err); } - } - return true; + }, + onerror: (err) => { + reject(err); + } + }); + }); } -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); +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'}]`); } -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'}]`); +function maybeChangeColors(span, left) { + if (CHANGE_COLOR) { + if (left < 5 * 60) { + $(span).addClass('npc_red-timer'); + } else if (left < 10 * 60) { + $(span).addClass('npc_orange-timer'); } 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'}]`); + $(span).removeClass('npc_orange-timer'); + $(span).removeClass('npc_red-timer'); } + } } -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 process(id, data) { + if (!data.npcs[id]) { + return; + } -function formatTimeSec(msec) { - return formatTimeMsec(msec).replace(/\..+/, ''); -} + 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; -function process(ts, loot_level) { - if (ts < 0) { - return; + if (remaining < 0) { + clearInterval(x); + 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 span = $('div.profile-status').find('div.profile-container').find('div.description').find('span.sub-desc'); + let html = $(span).html(); + if (html && currentLevel !== 5) { + const n = html.indexOf('('); + html = html.substring(0, n !== -1 ? n - 1 : html.length); + $(span).html(`${html} (Till loot level ${ROMAN[currentLevel === 4 ? 5 : 4]}: ${formatTime(remaining)})`); + } + }, 1000); } const newsContainerId = 'header-swiper-container'; @@ -258,254 +162,239 @@ 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; - - 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 (!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); + }); - if (ATTACK_TIMER) { - const pistolImg = 'Attack scheduled'; - div += `${pistolImg} `; + 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); + } + + setInterval(() => { + const now = Math.floor(new Date().getTime() / 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; + + if (SIDEBAR_TIMERS) { + const div = $(sidebar); + const span = div.find('span'); + let text; + if (currentLevel >= 4) { + text = elapsedTime < 0 ? 'Hosp' : `Loot level ${currentLevel}`; + } else { + text = formatTime(remaining); } - - 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); + $(span).text(text); + maybeChangeColors(span, remaining); + + div.find('a').first().css('text-decoration', data.npcs[id].clear ? 'none' : 'line-through'); + } + if (TOPBAR_TIMERS) { + const div = $(topbar); + const span = div.find('span'); + let text; + if (currentLevel >= 4) { + text = elapsedTime < 0 ? 'Hosp' : (isMobile() ? `LL ${currentLevel}` : `Loot lvl ${currentLevel}`); } else { - $('div.header-wrapper-bottom').prepend(div); - $('#topbarNpcTimers').find('a').css('color', '#069'); + text = isMobile() ? formatTime(remaining, 'minimal') : formatTime(remaining, 'short'); } - $('#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'); - } + $(span).text(text); + maybeChangeColors(span, remaining); + + div.find('a').first().css('text-decoration', data.npcs[id].clear ? 'none' : 'line-through'); + } + }); + const remainingTime = data.time.clear - now; if (SIDEBAR_TIMERS) { - const hide = GM_getValue('hideSidebarTimers'); - hideTimers(hide, data); + const sidebar = $('#npcTimerSidebarScheduledAttack'); + const span = sidebar.find('span'); + const text = remainingTime < 0 ? 'N/A' : formatTime(remainingTime); + sidebar.find('span').text(text); + + const scheduled = text !== 'N/A'; + if (scheduled) { + maybeChangeColors(span, remainingTime); + } + sidebar.attr('title', scheduled ? `Attack scheduled for ${formatTornTime(data.time.clear)}` : ''); } if (TOPBAR_TIMERS) { - const hide = GM_getValue('hideTopbarTimers'); - hideTimers(hide, data, false); + const topbar = $('#npcTimerTopbarScheduledAttack'); + const span = topbar.find('span'); + const text = remainingTime < 0 ? 'N/A' : (isMobile() ? formatTime(remainingTime, 'minimal') : formatTime(remainingTime, 'short')); + topbar.find('span').text(text); + + const scheduled = text !== 'N/A'; + if (text !== 'N/A') { + maybeChangeColors(span, remainingTime); + } + topbar.find('img').attr('title', scheduled ? `Attack scheduled for ${formatTornTime(data.time.clear)}` : 'Attack scheduled'); } - - 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(); - } - }) + }, 1000); } -/* 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()); -} +(function () { + 'use strict'; -async function addScheduledAttackTimer() { - const now = new Date().getTime(); - const lastUpdated = GM_getValue('attack_ts_last_updated'); - let attackTs = GM_getValue('attack_ts_cached'); + if ($(location).attr('href').includes('profiles.php')) { + const profileId = RegExp(/XID=(\d+)/).exec($(location).attr('href'))[1]; + updateData().then(process.bind(null, profileId)); + } - if (!attackTs || !lastUpdated || now - lastUpdated >= ATTACK_TIMER_API_DELAY) { - log('Calling attack timer API'); - await getAttackTime(); + const maybeAddTimers = () => { + if (SIDEBAR_TIMERS && $('#sidebar').size() > 0 || TOPBAR_TIMERS && $('div.header-wrapper-bottom').size() > 0) { + updateData().then(data => addNpcTimers(data)); } + }; + maybeAddTimers(); - 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)}` : ''); -} + // try again to handle new tab case + setTimeout(maybeAddTimers, 1000); +})(); -function startDisplayingScheduledAttack() { - if (displayAttackTimer > -1) { - clearInterval(displayAttackTimer); +// 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); + } + } + }); +}); - 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); -} - - +observer.observe(document.getElementById('header-root'), { + subtree: true, + childList: true, + characterData: false, + attributes: false, + attributeOldValue: false +}); -(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); -})(); +// Helper Functions -function log(data) { - if (LOGGING_ENABLED) console.log(data) +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`; } -// 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); - } - } - }); -}); +function formatTime(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}min` : minutes > 0 ? `${minutes}min ${seconds}s` : `${seconds}s`; + case 'minimal': + return (hours > 0 ? hours + ':' : '') + (hours > 0 || minutes > 0 ? pad(minutes, 2) + ':' : '') + pad(seconds, 2); + } +} -observer.observe(document.getElementById('header-root'), { subtree: true, childList: true, characterData: false, attributes: false, attributeOldValue: false }); +function pad(num, size) { + return String(num).padStart(size, '0'); +} \ No newline at end of file From 44b1b357542e8d29c75daf0bcf23a2e4053725f4 Mon Sep 17 00:00:00 2001 From: Christopher Stoll Date: Sun, 18 Jun 2023 21:04:20 -0400 Subject: [PATCH 02/17] Replace namespace which was removed by mistake --- npc_profile_loot_timer.user.js | 1 + 1 file changed, 1 insertion(+) diff --git a/npc_profile_loot_timer.user.js b/npc_profile_loot_timer.user.js index ff031a0..67f8c39 100644 --- a/npc_profile_loot_timer.user.js +++ b/npc_profile_loot_timer.user.js @@ -1,5 +1,6 @@ // ==UserScript== // @name Torn: Loot timer on NPC profile +// @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 From e80c10559286a37aca74b78ebbc48f915b739a2b Mon Sep 17 00:00:00 2001 From: Christopher Stoll Date: Sun, 18 Jun 2023 21:16:31 -0400 Subject: [PATCH 03/17] Remove unused @connect request --- npc_profile_loot_timer.user.js | 1 - 1 file changed, 1 deletion(-) diff --git a/npc_profile_loot_timer.user.js b/npc_profile_loot_timer.user.js index 67f8c39..e023ae3 100644 --- a/npc_profile_loot_timer.user.js +++ b/npc_profile_loot_timer.user.js @@ -7,7 +7,6 @@ // @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 yata.yt // @connect api.lzpt.io // @grant GM_xmlhttpRequest // @grant GM_addStyle From b3fd6ed558f076b70cd8c04a880d63eb43ab4213 Mon Sep 17 00:00:00 2001 From: Christopher Stoll Date: Sun, 18 Jun 2023 21:23:21 -0400 Subject: [PATCH 04/17] Run auto format --- npc_profile_loot_timer.user.js | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/npc_profile_loot_timer.user.js b/npc_profile_loot_timer.user.js index e023ae3..aae8d76 100644 --- a/npc_profile_loot_timer.user.js +++ b/npc_profile_loot_timer.user.js @@ -58,12 +58,12 @@ 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' + 'Fernando': 'Nando', 'Easter Bunny': 'Bunny' }[name] || name; }; const LOGGING_ENABLED = false; + function log(data) { if (LOGGING_ENABLED) console.log(data) } @@ -86,12 +86,9 @@ const updateData = async () => { return await new Promise((resolve, reject) => { GM_xmlhttpRequest({ - method: 'GET', - url: 'https://api.lzpt.io/loot/', - headers: { + method: 'GET', url: 'https://api.lzpt.io/loot/', headers: { 'Content-Type': 'application/json' - }, - onload: (response) => { + }, onload: (response) => { try { let data = JSON.parse(response.responseText); GM_setValue('cached_data', JSON.stringify(data)); @@ -101,8 +98,7 @@ const updateData = async () => { } catch (err) { reject(err); } - }, - onerror: (err) => { + }, onerror: (err) => { reject(err); } }); @@ -166,8 +162,9 @@ const addContentPadding = (add) => $('#mainContainer > div.content-wrapper').css const setTopbarPadding = (pad) => $('#topbarNpcTimers').css('padding-top', pad); function addNpcTimers(data) { - if (!data) + if (!data) { return; + } log('Adding NPC Timers') if (SIDEBAR_TIMERS && $('#sidebarNpcTimers').size() < 1) { @@ -365,11 +362,7 @@ const observer = new MutationObserver(function (mutations) { }); observer.observe(document.getElementById('header-root'), { - subtree: true, - childList: true, - characterData: false, - attributes: false, - attributeOldValue: false + subtree: true, childList: true, characterData: false, attributes: false, attributeOldValue: false }); From acb53f35dee752f002bf71115f856427f50dc835 Mon Sep 17 00:00:00 2001 From: Christopher Stoll Date: Sun, 18 Jun 2023 23:43:25 -0400 Subject: [PATCH 05/17] Use roman symbols to display loot level in top/sidebar --- npc_profile_loot_timer.user.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/npc_profile_loot_timer.user.js b/npc_profile_loot_timer.user.js index aae8d76..2e7dbb2 100644 --- a/npc_profile_loot_timer.user.js +++ b/npc_profile_loot_timer.user.js @@ -264,7 +264,7 @@ function addNpcTimers(data) { const span = div.find('span'); let text; if (currentLevel >= 4) { - text = elapsedTime < 0 ? 'Hosp' : `Loot level ${currentLevel}`; + text = elapsedTime < 0 ? 'Hosp' : `Loot level ${ROMAN[currentLevel]}`; } else { text = formatTime(remaining); } @@ -278,7 +278,7 @@ function addNpcTimers(data) { const span = div.find('span'); let text; if (currentLevel >= 4) { - text = elapsedTime < 0 ? 'Hosp' : (isMobile() ? `LL ${currentLevel}` : `Loot lvl ${currentLevel}`); + text = elapsedTime < 0 ? 'Hosp' : (isMobile() ? `LL ${ROMAN[currentLevel]}` : `Loot lvl ${ROMAN[currentLevel]}`); } else { text = isMobile() ? formatTime(remaining, 'minimal') : formatTime(remaining, 'short'); } From 33a75240397abb8aecd38bbb703e41b01909a57d Mon Sep 17 00:00:00 2001 From: Christopher Stoll Date: Mon, 19 Jun 2023 00:17:06 -0400 Subject: [PATCH 06/17] Change how the update loop runs to allow for the page to update the data --- npc_profile_loot_timer.user.js | 133 ++++++++++++++++++--------------- 1 file changed, 73 insertions(+), 60 deletions(-) diff --git a/npc_profile_loot_timer.user.js b/npc_profile_loot_timer.user.js index 2e7dbb2..57c12ee 100644 --- a/npc_profile_loot_timer.user.js +++ b/npc_profile_loot_timer.user.js @@ -68,7 +68,7 @@ function log(data) { if (LOGGING_ENABLED) console.log(data) } -const updateData = async () => { +async function getData() { try { const str_data = GM_getValue('cached_data'); const last_updated = GM_getValue('last_updated'); @@ -248,73 +248,86 @@ function addNpcTimers(data) { hideTimers(hide, false); } - setInterval(() => { - const now = Math.floor(new Date().getTime() / 1000); + if (!setup) { + setup = true; + renderTimes().catch(err => { + console.error(err); + }); + } +} - 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; - - 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 = formatTime(remaining); - } - $(span).text(text); - maybeChangeColors(span, remaining); +let setup = false; - div.find('a').first().css('text-decoration', data.npcs[id].clear ? 'none' : 'line-through'); - } - 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() ? formatTime(remaining, 'minimal') : formatTime(remaining, 'short'); - } - $(span).text(text); - maybeChangeColors(span, remaining); +async function renderTimes() { + const start = new Date().getTime(); + const data = await getData(); + const now = Math.floor(start / 1000); - div.find('a').first().css('text-decoration', data.npcs[id].clear ? 'none' : 'line-through'); - } - }); + 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 remainingTime = data.time.clear - now; if (SIDEBAR_TIMERS) { - const sidebar = $('#npcTimerSidebarScheduledAttack'); - const span = sidebar.find('span'); - const text = remainingTime < 0 ? 'N/A' : formatTime(remainingTime); - sidebar.find('span').text(text); - - const scheduled = text !== 'N/A'; - if (scheduled) { - maybeChangeColors(span, remainingTime); + const div = $(sidebar); + const span = div.find('span'); + let text; + if (currentLevel >= 4) { + text = elapsedTime < 0 ? 'Hosp' : `Loot level ${ROMAN[currentLevel]}`; + } else { + text = formatTime(remaining); } - sidebar.attr('title', scheduled ? `Attack scheduled for ${formatTornTime(data.time.clear)}` : ''); + $(span).text(text); + maybeChangeColors(span, remaining); + + div.find('a').first().css('text-decoration', data.npcs[id].clear ? 'none' : 'line-through'); } if (TOPBAR_TIMERS) { - const topbar = $('#npcTimerTopbarScheduledAttack'); - const span = topbar.find('span'); - const text = remainingTime < 0 ? 'N/A' : (isMobile() ? formatTime(remainingTime, 'minimal') : formatTime(remainingTime, 'short')); - topbar.find('span').text(text); - - const scheduled = text !== 'N/A'; - if (text !== 'N/A') { - maybeChangeColors(span, remainingTime); + 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() ? formatTime(remaining, 'minimal') : formatTime(remaining, 'short'); } - topbar.find('img').attr('title', scheduled ? `Attack scheduled for ${formatTornTime(data.time.clear)}` : 'Attack scheduled'); + $(span).text(text); + maybeChangeColors(span, remaining); + + div.find('a').first().css('text-decoration', data.npcs[id].clear ? 'none' : 'line-through'); } - }, 1000); + }); + + const remainingTime = data.time.clear - now; + if (SIDEBAR_TIMERS) { + const sidebar = $('#npcTimerSidebarScheduledAttack'); + const span = sidebar.find('span'); + const text = remainingTime < 0 ? 'N/A' : formatTime(remainingTime); + sidebar.find('span').text(text); + + const scheduled = text !== 'N/A'; + if (scheduled) { + maybeChangeColors(span, remainingTime); + } + sidebar.attr('title', scheduled ? `Attack scheduled for ${formatTornTime(data.time.clear)}` : ''); + } + if (TOPBAR_TIMERS) { + const topbar = $('#npcTimerTopbarScheduledAttack'); + const span = topbar.find('span'); + const text = remainingTime < 0 ? 'N/A' : (isMobile() ? formatTime(remainingTime, 'minimal') : formatTime(remainingTime, 'short')); + topbar.find('span').text(text); + + const scheduled = text !== 'N/A'; + if (text !== 'N/A') { + maybeChangeColors(span, remainingTime); + } + topbar.find('img').attr('title', scheduled ? `Attack scheduled for ${formatTornTime(data.time.clear)}` : 'Attack scheduled'); + } + + setTimeout(renderTimes, Math.max(100, start + 1000 - new Date().getTime())); } @@ -323,12 +336,12 @@ function addNpcTimers(data) { if ($(location).attr('href').includes('profiles.php')) { const profileId = RegExp(/XID=(\d+)/).exec($(location).attr('href'))[1]; - updateData().then(process.bind(null, profileId)); + getData().then(process.bind(null, profileId)); } const maybeAddTimers = () => { if (SIDEBAR_TIMERS && $('#sidebar').size() > 0 || TOPBAR_TIMERS && $('div.header-wrapper-bottom').size() > 0) { - updateData().then(data => addNpcTimers(data)); + getData().then(data => addNpcTimers(data)); } }; maybeAddTimers(); From 2905ae2ce4b4d85e97249387629bc89930b4fbcb Mon Sep 17 00:00:00 2001 From: Christopher Stoll Date: Mon, 19 Jun 2023 00:45:06 -0400 Subject: [PATCH 07/17] Auto retry data on failure --- npc_profile_loot_timer.user.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/npc_profile_loot_timer.user.js b/npc_profile_loot_timer.user.js index 57c12ee..cf46ed4 100644 --- a/npc_profile_loot_timer.user.js +++ b/npc_profile_loot_timer.user.js @@ -96,9 +96,11 @@ async function getData() { resolve(data); } catch (err) { + GM_setValue('last_updated', 0); reject(err); } }, onerror: (err) => { + GM_setValue('last_updated', 0); reject(err); } }); From 074e8ee6512897a3b95ae80ad3557a51bb46c892 Mon Sep 17 00:00:00 2001 From: Christopher Stoll Date: Mon, 19 Jun 2023 11:39:02 -0400 Subject: [PATCH 08/17] Bug fix clear status to correctly display when no attack is scheduled --- npc_profile_loot_timer.user.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/npc_profile_loot_timer.user.js b/npc_profile_loot_timer.user.js index cf46ed4..19f8303 100644 --- a/npc_profile_loot_timer.user.js +++ b/npc_profile_loot_timer.user.js @@ -273,6 +273,8 @@ async function renderTimes() { 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.time.clear <= now || data.npcs[id].clear) ? 'none' : 'line-through'; + if (SIDEBAR_TIMERS) { const div = $(sidebar); const span = div.find('span'); @@ -285,7 +287,7 @@ async function renderTimes() { $(span).text(text); maybeChangeColors(span, remaining); - div.find('a').first().css('text-decoration', data.npcs[id].clear ? 'none' : 'line-through'); + div.find('a').first().css('text-decoration', clearStatus); } if (TOPBAR_TIMERS) { const div = $(topbar); @@ -299,7 +301,7 @@ async function renderTimes() { $(span).text(text); maybeChangeColors(span, remaining); - div.find('a').first().css('text-decoration', data.npcs[id].clear ? 'none' : 'line-through'); + div.find('a').first().css('text-decoration', clearStatus); } }); From 297822cc328ff13d46f6699c2b6eac060422334d Mon Sep 17 00:00:00 2001 From: Christopher Stoll Date: Mon, 19 Jun 2023 12:00:12 -0400 Subject: [PATCH 09/17] Slight coding change to make intention more clear --- npc_profile_loot_timer.user.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/npc_profile_loot_timer.user.js b/npc_profile_loot_timer.user.js index 19f8303..fa5ffcd 100644 --- a/npc_profile_loot_timer.user.js +++ b/npc_profile_loot_timer.user.js @@ -306,13 +306,14 @@ async function renderTimes() { }); const remainingTime = data.time.clear - now; + const scheduled = remainingTime > 0; + if (SIDEBAR_TIMERS) { const sidebar = $('#npcTimerSidebarScheduledAttack'); const span = sidebar.find('span'); - const text = remainingTime < 0 ? 'N/A' : formatTime(remainingTime); + const text = scheduled ? 'N/A' : formatTime(remainingTime); sidebar.find('span').text(text); - const scheduled = text !== 'N/A'; if (scheduled) { maybeChangeColors(span, remainingTime); } @@ -321,11 +322,10 @@ async function renderTimes() { if (TOPBAR_TIMERS) { const topbar = $('#npcTimerTopbarScheduledAttack'); const span = topbar.find('span'); - const text = remainingTime < 0 ? 'N/A' : (isMobile() ? formatTime(remainingTime, 'minimal') : formatTime(remainingTime, 'short')); + const text = scheduled ? 'N/A' : (isMobile() ? formatTime(remainingTime, 'minimal') : formatTime(remainingTime, 'short')); topbar.find('span').text(text); - const scheduled = text !== 'N/A'; - if (text !== 'N/A') { + if (scheduled) { maybeChangeColors(span, remainingTime); } topbar.find('img').attr('title', scheduled ? `Attack scheduled for ${formatTornTime(data.time.clear)}` : 'Attack scheduled'); From 044b711b77de27f674b9b2661f175a1a88315e35 Mon Sep 17 00:00:00 2001 From: Christopher Stoll Date: Mon, 19 Jun 2023 13:24:29 -0400 Subject: [PATCH 10/17] Bug fix attack schedule timer, add green color for loot level 4 & 5 when CHANGE_COLOR is enabled --- npc_profile_loot_timer.user.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/npc_profile_loot_timer.user.js b/npc_profile_loot_timer.user.js index fa5ffcd..362eff1 100644 --- a/npc_profile_loot_timer.user.js +++ b/npc_profile_loot_timer.user.js @@ -36,6 +36,10 @@ GM_addStyle(` .npc_red-timer { color: red; } + + .npc_green-timer { + color: green; + } .npc_show-hide { color: #069; @@ -113,15 +117,16 @@ function hideTimers(hide, sidebar) { $(`#showHide${sidebar ? 'Side' : 'Top'}barTimers`).text(`[${hide ? sidebar ? 'show' : 'snow NPC timers' : 'hide'}]`); } -function maybeChangeColors(span, left) { +function maybeChangeColors(span, time, level = 3) { if (CHANGE_COLOR) { - if (left < 5 * 60) { + if (level < 4 && time < 5 * 60) { $(span).addClass('npc_red-timer'); - } else if (left < 10 * 60) { + } else if (level < 4 && time < 10 * 60) { $(span).addClass('npc_orange-timer'); + } else if (level >= 4) { + $(span).addClass('npc_green-timer'); } else { - $(span).removeClass('npc_orange-timer'); - $(span).removeClass('npc_red-timer'); + $(span).removeClass('npc_orange-timer npc_red-timer npc_green-timer'); } } } @@ -285,7 +290,7 @@ async function renderTimes() { text = formatTime(remaining); } $(span).text(text); - maybeChangeColors(span, remaining); + maybeChangeColors(span, remaining, currentLevel); div.find('a').first().css('text-decoration', clearStatus); } @@ -299,7 +304,7 @@ async function renderTimes() { text = isMobile() ? formatTime(remaining, 'minimal') : formatTime(remaining, 'short'); } $(span).text(text); - maybeChangeColors(span, remaining); + maybeChangeColors(span, remaining, currentLevel); div.find('a').first().css('text-decoration', clearStatus); } @@ -311,7 +316,7 @@ async function renderTimes() { if (SIDEBAR_TIMERS) { const sidebar = $('#npcTimerSidebarScheduledAttack'); const span = sidebar.find('span'); - const text = scheduled ? 'N/A' : formatTime(remainingTime); + const text = scheduled ? formatTime(remainingTime) : 'N/A'; sidebar.find('span').text(text); if (scheduled) { @@ -322,7 +327,7 @@ async function renderTimes() { if (TOPBAR_TIMERS) { const topbar = $('#npcTimerTopbarScheduledAttack'); const span = topbar.find('span'); - const text = scheduled ? 'N/A' : (isMobile() ? formatTime(remainingTime, 'minimal') : formatTime(remainingTime, 'short')); + const text = scheduled ? (isMobile() ? formatTime(remainingTime, 'minimal') : formatTime(remainingTime, 'short')) : 'N/A'; topbar.find('span').text(text); if (scheduled) { From 4f2940be1bf8c104296645f3d262257d30b1a2f7 Mon Sep 17 00:00:00 2001 From: Christopher Stoll Date: Mon, 19 Jun 2023 14:30:44 -0400 Subject: [PATCH 11/17] Add a tooltip for profiles to show attack order, bug fix when page loaded with NID --- npc_profile_loot_timer.user.js | 50 +++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/npc_profile_loot_timer.user.js b/npc_profile_loot_timer.user.js index 362eff1..ee5e125 100644 --- a/npc_profile_loot_timer.user.js +++ b/npc_profile_loot_timer.user.js @@ -147,12 +147,32 @@ function process(id, data) { return; } - const span = $('div.profile-status').find('div.profile-container').find('div.description').find('span.sub-desc'); - let html = $(span).html(); - if (html && currentLevel !== 5) { - const n = html.indexOf('('); - html = html.substring(0, n !== -1 ? n - 1 : html.length); - $(span).html(`${html} (Till loot level ${ROMAN[currentLevel === 4 ? 5 : 4]}: ${formatTime(remaining)})`); + 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].clear) { + 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].clear) { + subDesc.attr('title', `Attack ${location} at ${formatTornTime(data.time.clear)}`); + } else { + subDesc.attr('title', `Attack Not Scheduled`); + } } }, 1000); } @@ -287,7 +307,7 @@ async function renderTimes() { if (currentLevel >= 4) { text = elapsedTime < 0 ? 'Hosp' : `Loot level ${ROMAN[currentLevel]}`; } else { - text = formatTime(remaining); + text = formatCountdown(remaining); } $(span).text(text); maybeChangeColors(span, remaining, currentLevel); @@ -301,7 +321,7 @@ async function renderTimes() { if (currentLevel >= 4) { text = elapsedTime < 0 ? 'Hosp' : (isMobile() ? `LL ${ROMAN[currentLevel]}` : `Loot lvl ${ROMAN[currentLevel]}`); } else { - text = isMobile() ? formatTime(remaining, 'minimal') : formatTime(remaining, 'short'); + text = isMobile() ? formatCountdown(remaining, 'minimal') : formatCountdown(remaining, 'short'); } $(span).text(text); maybeChangeColors(span, remaining, currentLevel); @@ -316,7 +336,7 @@ async function renderTimes() { if (SIDEBAR_TIMERS) { const sidebar = $('#npcTimerSidebarScheduledAttack'); const span = sidebar.find('span'); - const text = scheduled ? formatTime(remainingTime) : 'N/A'; + const text = scheduled ? formatCountdown(remainingTime) : 'N/A'; sidebar.find('span').text(text); if (scheduled) { @@ -327,7 +347,7 @@ async function renderTimes() { if (TOPBAR_TIMERS) { const topbar = $('#npcTimerTopbarScheduledAttack'); const span = topbar.find('span'); - const text = scheduled ? (isMobile() ? formatTime(remainingTime, 'minimal') : formatTime(remainingTime, 'short')) : 'N/A'; + const text = scheduled ? (isMobile() ? formatCountdown(remainingTime, 'minimal') : formatCountdown(remainingTime, 'short')) : 'N/A'; topbar.find('span').text(text); if (scheduled) { @@ -344,8 +364,12 @@ async function renderTimes() { 'use strict'; if ($(location).attr('href').includes('profiles.php')) { - const profileId = RegExp(/XID=(\d+)/).exec($(location).attr('href'))[1]; - getData().then(process.bind(null, profileId)); + 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 = () => { @@ -395,7 +419,7 @@ function formatTornTime(time) { 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 formatTime(sec, mode = 'long') { +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); From 256e64705011e527cb140dbb25a78fadd1e4a2d6 Mon Sep 17 00:00:00 2001 From: Christopher Stoll Date: Thu, 5 Oct 2023 23:13:36 -0400 Subject: [PATCH 12/17] Implement new loot rangers changes that add time.clear.attack boolean to better represent NPCs as part of / not included in a clear while its taking place. Rename from npc_profile_loot_timer.user.js to npc_loot_timers.js as well as change script name to Torn: Loot Timers to better reflect the scripts overall purpose. --- ...e_loot_timer.user.js => npc_loot_timers.js | 49 ++++++++++--------- 1 file changed, 27 insertions(+), 22 deletions(-) rename npc_profile_loot_timer.user.js => npc_loot_timers.js (92%) diff --git a/npc_profile_loot_timer.user.js b/npc_loot_timers.js similarity index 92% rename from npc_profile_loot_timer.user.js rename to npc_loot_timers.js index ee5e125..1861056 100644 --- a/npc_profile_loot_timer.user.js +++ b/npc_loot_timers.js @@ -1,5 +1,5 @@ // ==UserScript== -// @name Torn: Loot timer on NPC profile +// @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. @@ -117,17 +117,22 @@ function hideTimers(hide, sidebar) { $(`#showHide${sidebar ? 'Side' : 'Top'}barTimers`).text(`[${hide ? sidebar ? 'show' : 'snow NPC timers' : 'hide'}]`); } -function maybeChangeColors(span, time, level = 3) { - if (CHANGE_COLOR) { - if (level < 4 && time < 5 * 60) { - $(span).addClass('npc_red-timer'); - } else if (level < 4 && time < 10 * 60) { - $(span).addClass('npc_orange-timer'); - } else if (level >= 4) { - $(span).addClass('npc_green-timer'); - } else { - $(span).removeClass('npc_orange-timer npc_red-timer npc_green-timer'); +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'); } } @@ -170,6 +175,8 @@ function process(id, data) { if (data.time.clear > now && data.npcs[id].clear) { subDesc.attr('title', `Attack ${location} at ${formatTornTime(data.time.clear)}`); + } else if (data.time.attack) { + subDesc.attr('title', 'Attack Right Now'); } else { subDesc.attr('title', `Attack Not Scheduled`); } @@ -298,7 +305,7 @@ async function renderTimes() { 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.time.clear <= now || data.npcs[id].clear) ? 'none' : 'line-through'; + const clearStatus = data.npcs[id].clear ? 'none' : 'line-through'; if (SIDEBAR_TIMERS) { const div = $(sidebar); @@ -336,24 +343,22 @@ async function renderTimes() { if (SIDEBAR_TIMERS) { const sidebar = $('#npcTimerSidebarScheduledAttack'); const span = sidebar.find('span'); - const text = scheduled ? formatCountdown(remainingTime) : 'N/A'; + const text = scheduled ? formatCountdown(remainingTime) : data.time.attack ? 'NOW' : 'N/A'; sidebar.find('span').text(text); - if (scheduled) { - maybeChangeColors(span, remainingTime); - } - sidebar.attr('title', scheduled ? `Attack scheduled for ${formatTornTime(data.time.clear)}` : ''); + maybeChangeColors(span, remainingTime, undefined, scheduled || data.time.attack); + + sidebar.attr('title', scheduled ? `Attack scheduled for ${formatTornTime(data.time.clear)}` : data.time.attack ? 'Attack now' : 'No attack scheduled'); } if (TOPBAR_TIMERS) { const topbar = $('#npcTimerTopbarScheduledAttack'); const span = topbar.find('span'); - const text = scheduled ? (isMobile() ? formatCountdown(remainingTime, 'minimal') : formatCountdown(remainingTime, 'short')) : 'N/A'; + const text = scheduled ? (isMobile() ? formatCountdown(remainingTime, 'minimal') : formatCountdown(remainingTime, 'short')) : data.time.attack ? 'NOW' : 'N/A'; topbar.find('span').text(text); - if (scheduled) { - maybeChangeColors(span, remainingTime); - } - topbar.find('img').attr('title', scheduled ? `Attack scheduled for ${formatTornTime(data.time.clear)}` : 'Attack scheduled'); + maybeChangeColors(span, remainingTime, undefined, scheduled || data.time.attack); + + topbar.attr('title', scheduled ? `Attack scheduled for ${formatTornTime(data.time.clear)}` : data.time.attack ? 'Attack now' : 'No attack scheduled'); } setTimeout(renderTimes, Math.max(100, start + 1000 - new Date().getTime())); From 91e44e32f4307f5597780bf6da4a160fc4dd6d00 Mon Sep 17 00:00:00 2001 From: Christopher Stoll Date: Sat, 2 Dec 2023 11:13:28 -0500 Subject: [PATCH 13/17] implement new time.reason field to indicate if attacking is on hold due to an event/similar --- npc_loot_timers.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/npc_loot_timers.js b/npc_loot_timers.js index 1861056..2a981c3 100644 --- a/npc_loot_timers.js +++ b/npc_loot_timers.js @@ -80,7 +80,14 @@ async function getData() { const data = JSON.parse(str_data); const now = new Date().getTime(); - if (now - last_updated < 15 * 60 * 1000 && ((data.time.clear !== 0 && data.time.clear >= now / 1000) || now - last_updated <= 60 * 1000)) { + + 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) { @@ -177,6 +184,8 @@ function process(id, data) { 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`); } @@ -343,22 +352,22 @@ async function renderTimes() { if (SIDEBAR_TIMERS) { const sidebar = $('#npcTimerSidebarScheduledAttack'); const span = sidebar.find('span'); - const text = scheduled ? formatCountdown(remainingTime) : data.time.attack ? 'NOW' : 'N/A'; + 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' : 'No attack scheduled'); + 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' : 'N/A'; + 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' : 'No attack scheduled'); + 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())); From 60b4f433bb6b8b8dcbcfac8453d49ac14f1647cb Mon Sep 17 00:00:00 2001 From: Christopher Stoll Date: Sat, 2 Dec 2023 11:14:31 -0500 Subject: [PATCH 14/17] Switch to using new next field rather than deprecated clear field --- npc_loot_timers.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/npc_loot_timers.js b/npc_loot_timers.js index 2a981c3..05b7911 100644 --- a/npc_loot_timers.js +++ b/npc_loot_timers.js @@ -169,7 +169,7 @@ function process(id, data) { let location = 0; for (const l of data.order) { - if (data.npcs[l].clear) { + if (data.npcs[l].next) { location++; } if (l === id) { @@ -180,7 +180,7 @@ function process(id, data) { subDesc.html(`${subHtml} (Loot Level ${ROMAN[currentLevel === 4 ? 5 : 4]} in ${formatCountdown(remaining)})`); } - if (data.time.clear > now && data.npcs[id].clear) { + 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'); @@ -314,7 +314,7 @@ async function renderTimes() { 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].clear ? 'none' : 'line-through'; + const clearStatus = data.npcs[id].next ? 'none' : 'line-through'; if (SIDEBAR_TIMERS) { const div = $(sidebar); From fd7747e7c40140ace3f19aa8d31507c8434aa54c Mon Sep 17 00:00:00 2001 From: Christopher Stoll Date: Sat, 2 Dec 2023 11:15:41 -0500 Subject: [PATCH 15/17] Switch to m rather than min --- npc_loot_timers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npc_loot_timers.js b/npc_loot_timers.js index 05b7911..f8f8048 100644 --- a/npc_loot_timers.js +++ b/npc_loot_timers.js @@ -442,7 +442,7 @@ function formatCountdown(sec, mode = 'long') { case 'long': return (hours > 0 ? hours + 'h ' : '') + (hours > 0 || minutes > 0 ? minutes + 'min ' : '') + seconds + 's'; case 'short': - return hours > 0 ? `${hours}h ${minutes}min` : minutes > 0 ? `${minutes}min ${seconds}s` : `${seconds}s`; + 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); } From 6440fa4ee467b38677d1771d765cb51a11058593 Mon Sep 17 00:00:00 2001 From: Christopher Stoll Date: Sat, 2 Dec 2023 11:17:35 -0500 Subject: [PATCH 16/17] Rename file to npc_loot_timers.user.js --- npc_loot_timers.js => npc_loot_timers.user.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename npc_loot_timers.js => npc_loot_timers.user.js (100%) diff --git a/npc_loot_timers.js b/npc_loot_timers.user.js similarity index 100% rename from npc_loot_timers.js rename to npc_loot_timers.user.js From 0c19a8231ccdc2b51a3034028dcd1ee25a63a0e1 Mon Sep 17 00:00:00 2001 From: Christopher Stoll Date: Sat, 2 Dec 2023 11:22:46 -0500 Subject: [PATCH 17/17] Fix spacing on PC, mobile may be messed up --- npc_loot_timers.user.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/npc_loot_timers.user.js b/npc_loot_timers.user.js index f8f8048..2a33b52 100644 --- a/npc_loot_timers.user.js +++ b/npc_loot_timers.user.js @@ -247,7 +247,7 @@ function addNpcTimers(data) { const npc_html = data.order.map(id => [id, data.npcs[id].name]).map(([id, name]) => (` ${SHORT_NAME(name)}:  - + `)).join(''); @@ -259,7 +259,7 @@ function addNpcTimers(data) { Attack scheduled - + ${npc_html}