Skip to content

Instantly share code, notes, and snippets.

@nabbynz
Last active June 11, 2022 02:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nabbynz/b2c81a69808ee2f9d847c3b1bffdace3 to your computer and use it in GitHub Desktop.
Save nabbynz/b2c81a69808ee2f9d847c3b1bffdace3 to your computer and use it in GitHub Desktop.
Maps Rater
// ==UserScript==
// @name Maps Rater
// @description Rate maps from the /maps page. Ratings get updated on the server when you next play that map. Shows Win % for each map.
// @version 0.0.12
// @include https://tagpro.koalabeast.com/maps
// @include https://tagpro.koalabeast.com/game
// @include https://tagpro.koalabeast.com/game?*
// @updateURL https://gist.github.com/nabbynz/b2c81a69808ee2f9d847c3b1bffdace3/raw/Maps_Rater.user.js
// @downloadURL https://gist.github.com/nabbynz/b2c81a69808ee2f9d847c3b1bffdace3/raw/Maps_Rater.user.js
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_addStyle
// @author nabby
// ==/UserScript==
console.log('START: ' + GM_info.script.name + ' (v' + GM_info.script.version + ' by ' + GM_info.script.author + ')');
tagpro.ready(function() {
//--------------------------
// Options...
//--------------------------
const showMyLastPlayed = true; //Show when each map was last played
const showMyPlays = true; //Not entirely sure (How many times each map has been played by me? This data comes from the server)
const showMyWins = true; //Win % on each map (since this script was installed)
const showTimeline = true; //Win/Loss timeline for the maps currently in rotation (since this script was installed)
const timelineGames = 50; //# of games to show on the timeline
const minimumGameTime = 30; //Minimum # of seconds for the game to count for timeline save
const showSaveAttempt = true; //Shows a "Save Attempt" text in the top-right corner in game
//--------------------------
let mapRatings = GM_getValue('mapRatings', {});
let joinTime;
if (location.pathname === '/game') {
let mapName, mapAuthor, tpRating = null, tpPlays = null;
let isSaveAttempt = false;
tagpro.socket.on('map', function(data) {
if (!data.info.name.includes('Mirrored')) {
mapName = data.info.name.trim();
mapAuthor = data.info.author.trim();
}
});
tagpro.socket.on('mapRating', function(data) {
tpRating = data.rating;
tpPlays = data.plays;
});
tagpro.socket.on('time', function(data) {
if (data.state === 3) { //before the actual start
joinTime = Date.now();
} else if (data.state === 1) { //game has started
if (!joinTime) joinTime = Date.now(); //joined mid-game
} else if (data.state === 5) { //overtime
if (!joinTime) joinTime = Date.now(); //joined in overtime
}
});
tagpro.socket.on('spectator', function(spectator) {
if (!spectator.type) joinTime = Date.now(); //joined from spec
});
tagpro.socket.on('end', function() {
let endTime = Date.now(); //actual end of game time
let myTimePlayed = (endTime - (joinTime || endTime)) / 1000; //how long we played for
if (mapName && !tagpro.spectator && (myTimePlayed > minimumGameTime) && (!tagpro.group.socket || tagpro.group.socket && !tagpro.group.socket.connected)) {
if (!mapRatings.hasOwnProperty(mapName)) {
mapRatings[mapName] = { mapAuthor:'Unknown', rating:null, plays:null, lastRated:null, lastPlayed:null, lastResults:[], wins:0, losses:0 };
}
if (tpRating !== null && tpPlays !== null) { // these values were only received if we're logged in (or possibly if the map is new and doesn't appear on the /maps page yet???). tpRating is `undefined` if our rating is expired.
if (+tpRating !== +mapRatings[mapName].rating) {
if (mapRatings[mapName].rating !== null && (+mapRatings[mapName].rating === -1 || +mapRatings[mapName].rating === 0 || +mapRatings[mapName].rating === 1)) { //need to update our rating to the server (note: +null === 0, +undefined === NaN)
tagpro.socket.emit('mapRating', +mapRatings[mapName].rating); //don't rely on just changing the val() - it won't always work as expected :(
$("select#mapRating").val(mapRatings[mapName].rating);
mapRatings[mapName].serverRating = mapRatings[mapName].rating;
mapRatings[mapName].lastRated = Date.now();
} else if (+tpRating === -1 || +tpRating === 0 || +tpRating === 1) { //save the rating from server so we can use it on the maps page
mapRatings[mapName].rating = tpRating;
mapRatings[mapName].serverRating = tpRating;
}
} else {
if (+tpRating === -1 || +tpRating === 0 || +tpRating === 1) {
mapRatings[mapName].serverRating = tpRating;
}
}
mapRatings[mapName].plays = tpPlays;
}
mapRatings[mapName].lastPlayed = Date.now();
if (!mapRatings[mapName].hasOwnProperty('lastResults')) mapRatings[mapName].lastResults = [];
if (!mapRatings[mapName].hasOwnProperty('mapAuthor')) mapRatings[mapName].mapAuthor = mapAuthor || 'Unknown';
if (tagpro.winner === 'red' && tagpro.players[tagpro.playerId].team === 1 || tagpro.winner === 'blue' && tagpro.players[tagpro.playerId].team === 2) {
mapRatings[mapName].wins++;
mapRatings[mapName].lastResults.push({ timePlayed:Date.now(), result:1, team:tagpro.players[tagpro.playerId].team, scoreRed:tagpro.score.r, scoreBlue:tagpro.score.b, saveAttempt:isSaveAttempt });
} else {
if (!isSaveAttempt) mapRatings[mapName].losses++;
mapRatings[mapName].lastResults.push({ timePlayed:Date.now(), result:0, team:tagpro.players[tagpro.playerId].team, scoreRed:tagpro.score.r, scoreBlue:tagpro.score.b, saveAttempt:isSaveAttempt });
}
while (mapRatings[mapName].lastResults.length > 5) {
mapRatings[mapName].lastResults.shift();
}
GM_setValue('mapRatings', mapRatings);
}
});
$("select#mapRating").change(function() {
if (mapName) {
mapRatings[mapName].rating = $(this).val();
mapRatings[mapName].serverRating = $(this).val();
mapRatings[mapName].lastRated = Date.now();
GM_setValue('mapRatings', mapRatings);
}
});
//this listens to the chat for the "This is a save attempt!" message upon joining a game (for 1.5s) then removes itself. Or if we're speccing we keep listening until we join.
let clearable_removeChatListener;
function handleChat(data) {
if (data.from === null && data.message.startsWith('This is a save attempt!')) {
isSaveAttempt = true;
removeChatListener();
if (showSaveAttempt && !tagpro.ui.sprites.saveAttempt) {
tagpro.ui.sprites.saveAttempt = tagpro.renderer.prettyText("Save Attempt");
tagpro.ui.sprites.saveAttempt.tint = 0xFF5500;
tagpro.ui.sprites.saveAttempt.anchor.x = 1;
tagpro.ui.sprites.saveAttempt.x = tagpro.renderer.vpWidth - 6;
tagpro.ui.sprites.saveAttempt.y = 10;
//tagpro.ui.sprites.saveAttempt.alpha = 0.7;
tagpro.renderer.layers.ui.addChild(tagpro.ui.sprites.saveAttempt);
}
}
}
function removeChatListener(delay=100) {
setTimeout(function() {
if (!tagpro.spectator) {
tagpro.rawSocket.removeListener('chat', handleChat);
clearInterval( clearable_removeChatListener );
}
}, delay);
}
tagpro.rawSocket.on('chat', handleChat);
clearable_removeChatListener = setInterval(function() {
if (!tagpro.spectator) {
removeChatListener(1500);
}
}, 2000);
} else if (location.pathname === '/maps') {
const serverRatingUnknown = '<span class="MR_RatingText" style="color:#777; opacity:1;" title="Waiting for vote">&#9679;</span>';
const serverRatingLike = '<span class="MR_RatingText" style="color:#7bd117; opacity:1;" title="Like">&#9679;</span>'; //8bc34a
const serverRatingNeutral = '<span class="MR_RatingText" style="color:#0e8ae0; opacity:1;" title="Neutral">&#9679;</span>'; //
const serverRatingDislike = '<span class="MR_RatingText" style="color:#c73939; opacity:1;" title="Dislike">&#9679;</span>'; //c34a4a
const serverRatingsMatch = '<span class="MR_RatingMatch" style="color:#0f0; opacity:0.5;" title="This rating matches the rating on the server">&#10004;</span>'; //tick:&#10004; thumbsup:&#128077;
const serverRatingsDontMatch = '<span class="MR_RatingMatch" style="color:#f90; opacity:0.75; font-size:13px;" title="This rating will be updated the next time the map is played">&#8634;</span>'; //cross:&#10007; hourglass:&#9203; watch:&#9201;
$('table').hide();
$('.map-detail').hide(0);
$.getJSON('/maps.json', function(mapsJSON) {
//console.log('mapsJSON:', mapsJSON);
//bind events...
$('.js-map-prev, .js-map-next').on('click', function() {
let currentTab = $('ul.tab-list').find('li.active').data('target');
let id = $(currentTab).find('.dot.active').attr('id');
$(currentTab).find('table tr').css('background', '');
$(currentTab).find('a#'+id).parent('td').parent('tr').css('background', '#111');
updateMapDetails();
});
$('table tbody a').on('click', function(e) {
$(this).parents('table').find('tr').css('background', '');
$(this).parent('p').parent('td').parent('tr').css('background', '#111');
//updateMapDetails(); //this gets fired by the 'circle' click listener
});
$('circle').on('click', function(e) {
let currentTab = $('ul.tab-list').find('li.active').data('target');
let id = $(currentTab).find('.dot.active').attr('id');
$(currentTab).find('table tr').css('background', '');
$(currentTab).find('a#'+id).parent('td').parent('tr').css('background', '#111');
updateMapDetails();
});
$('circle').on('mouseenter', function() {
let currentTab = $('ul.tab-list').find('li.active').data('target');
let isActive = $(this).attr('class').includes('active'); //the version of jQuery on the server doesn't support hasClass on svg elements so we're doing it this way
if (!isActive) {
$(this).attr('r', 7);
$(currentTab).find('a#'+this.id).parent('td').parent('tr').css('background', '#131');
}
}).on('mouseleave', function() {
let currentTab = $('ul.tab-list').find('li.active').data('target');
let isActive = $(this).attr('class').includes('active');
if (!isActive) {
$(this).attr('r', 5);
$(currentTab).find('a#'+this.id).parent('td').parent('tr').css('background', '');
}
});
$('table tbody tr').on('mouseenter', function() {
let currentTab = $('ul.tab-list').find('li.active').data('target');
let id = $(this).find('a').attr('id');
$(currentTab).find('circle#'+id).attr('r', 7);
}).on('mouseleave', function() {
let currentTab = $('ul.tab-list').find('li.active').data('target');
let id = $(this).find('a').attr('id');
let $circle = $(currentTab).find('circle#'+id);
let isActive = $circle.attr('class').includes('active');
if (isActive) $circle.attr('r', 10);
else $circle.attr('r', 5);
});
$('table').on('click', 'th', function(e, preventReverse) {
let order = GM_getValue('order', true);
let sortby = GM_getValue('sortby', 2);
let $table = $(this).parents('table');
GM_setValue('sortby', $(this).index());
$table.find('th').css('text-decoration', 'none');
$(this).css('text-decoration', 'underline');
let tbody = $table.find('tbody');
let rows = tbody.find('tr').toArray().sort(comparer($(this).index()));
if ( (sortby === $(this).index() && order && preventReverse !== true) || (sortby !== $(this).index() && !order && preventReverse !== true) || (preventReverse && !order) ) rows = rows.reverse();
if (sortby === $(this).index() && preventReverse !== true) GM_setValue('order', !order);
for (let i=0; i<rows.length; i++) { tbody.append(rows[i]); }
});
//begin table enhancement...
$('table').each(function() {
let $tbody = $(this).find('tbody');
let $rows = $tbody.find('tr');
if (showMyWins) $(this).find('thead tr').find('th:eq(6)').after('<th style="text-align:center; background:#6f9c34;">Win %</th>');
if (showMyLastPlayed) $(this).find('thead tr').find('th:eq(6)').after('<th style="text-align:center; background:#6f9c34;">Last</th>');
if (showMyPlays) $(this).find('thead tr').find('th:eq(6)').after('<th style="text-align:center; background:#6f9c34;">#Plays</th>');
$(this).find('thead tr').find('th:eq(6)').after('<th style="text-align:center; background:#6f9c34;">My Vote</th>');
$rows.each(function(index) {
let mapName = $(this).find('.likes').prev().prev().text().trim();
let isMirrored = mapName.endsWith(' Mirrored');
if (isMirrored) { //we can't rate mirrored maps and they just take up space
$(this).remove();
} else {
let likes = parseInt( $(this).find('.likes').text().replace(/,/g, ''), 10 );
let indifferents = parseInt( $(this).find('.indifferents').text().replace(/,/g, ''), 10 );
let dislikes = parseInt( $(this).find('.dislikes').text().replace(/,/g, ''), 10 );
let plays = parseInt( $(this).find('.dislikes').next('td').text().replace(/,/g, ''), 10 );
let totalUsers = likes + indifferents + dislikes;
let likes_pc = likes / (totalUsers || 1) * 100;
let indifferents_pc = indifferents / (totalUsers || 1) * 100;
let dislikes_pc = dislikes / (totalUsers || 1) * 100;
let score_pc = likes / ((likes + dislikes) || 1) * 100;
if (!mapRatings.hasOwnProperty(mapName)) {
mapRatings[mapName] = { mapAuthor:'Unknown', rating:null, plays:null, lastRated:null, lastPlayed:null, lastResults:[], wins:0, losses:0, serverRating:null };
}
$(this).find('a.map-list-entry').parent('td').data('sortby', mapName);
$(this).find('td.likes').prev('td').html((score_pc).toFixed(1)+'%').data('sortby', score_pc).attr('title', 'Score = Likes / (Likes + Dislikes)');
$(this).find('td.likes').html(likes_pc.toFixed(1)+'%').data('sortby', likes_pc).attr('title', likes);
$(this).find('td.indifferents').html(indifferents_pc.toFixed(1)+'%').data('sortby', indifferents_pc).attr('title', indifferents);
$(this).find('td.dislikes').html(dislikes_pc.toFixed(1)+'%').data('sortby', dislikes_pc).attr('title', dislikes);
$(this).find('td.dislikes').next('td').data('sortby', plays);
$(this).find('td.dislikes').next('td').next('td').data('sortby', totalUsers);
$(this).find('span.ratio-bar').parent('td').data('sortby', likes_pc);
$(this).find('a.map-list-entry').before('<p class="MR_MapName" title="' + mapName + (mapRatings[mapName].mapAuthor ? ' by ' + mapRatings[mapName].mapAuthor : '') + '"></p>');
$(this).find('.MR_MapName').append( $(this).find('a.map-list-entry') );
let lastPlayed = mapRatings[mapName].lastPlayed ? new Date(mapRatings[mapName].lastPlayed).toDateString() + ' @ ' + new Date(mapRatings[mapName].lastPlayed).toLocaleTimeString() : '';
//check our rating matches the rating from the server...
let serverRatingText = serverRatingUnknown;
let serverRatingMatchText = '<span class="MR_RatingMatch" style="font-size:13px;" title="Waiting for rating from server (next play)">?</span>';
if (mapRatings[mapName].hasOwnProperty('rating') && mapRatings[mapName].hasOwnProperty('serverRating')) {
if (mapRatings[mapName].rating !== null) {
if (+mapRatings[mapName].rating === 1) serverRatingText = serverRatingLike;
else if (+mapRatings[mapName].rating === 0) serverRatingText = serverRatingNeutral;
else if (+mapRatings[mapName].rating === -1) serverRatingText = serverRatingDislike;
else serverRatingText = serverRatingUnknown;
}
if (mapRatings[mapName].rating !== null && mapRatings[mapName].serverRating !== null && (+mapRatings[mapName].serverRating === -1 || +mapRatings[mapName].serverRating === 0 || +mapRatings[mapName].serverRating === 1)) {
if (+mapRatings[mapName].serverRating === +mapRatings[mapName].rating) serverRatingMatchText = serverRatingsMatch;
else serverRatingMatchText = serverRatingsDontMatch;
}
}
//add new columns...
if (showMyWins) {
let sortby = mapRatings[mapName].wins > 0 ? mapRatings[mapName].wins / ((mapRatings[mapName].wins + mapRatings[mapName].losses) || 1) : mapRatings[mapName].losses > 0 ? -mapRatings[mapName].losses : -999999;
$(this).find('td:eq(6)').after('<td class="lastResult" style="text-align:center;" data-sortby="' + sortby + '" data-sortby2="' + mapRatings[mapName].wins + '"><div title="' + (mapRatings[mapName].wins || 0) + ' Wins, ' + (mapRatings[mapName].losses || 0) + ' Losses">' + (mapRatings[mapName].wins >= 0 ? (mapRatings[mapName].wins / ((mapRatings[mapName].wins + mapRatings[mapName].losses) || 1) * 100).toFixed(1) + '%' : '-') + '</div></td>');
addLastResults(this, mapName);
}
if (showMyLastPlayed) $(this).find('td:eq(6)').after('<td class="lastPlayed" style="text-align:center; font-size:10px; width:90px;" data-sortby="' + (mapRatings[mapName].lastPlayed ? mapRatings[mapName].lastPlayed : -1) + '" title="' + lastPlayed + '">' + (mapRatings[mapName].lastPlayed ? dayjs(mapRatings[mapName].lastPlayed).from() : '-') + '</td>');
if (showMyPlays) $(this).find('td:eq(6)').after('<td style="text-align:center;"data-sortby="' + ($.isNumeric(mapRatings[mapName].plays) ? mapRatings[mapName].plays : 0) + '">' + ($.isNumeric(mapRatings[mapName].plays) ? mapRatings[mapName].plays + 1 : '-') + '</td>');
$(this).find('td:eq(6)').after('<td style="text-align:center;" data-sortby="' + ($.isNumeric(mapRatings[mapName].rating) ? mapRatings[mapName].rating : 999) + '"><div class="MR_RatingContainer">' + serverRatingText + '<select class="MR_Rating" value="" data-mapname="' + mapName + '" data-serverrating="' + mapRatings[mapName].serverRating + '"><option value="1">Like</option><option value="0">Neutral</option><option value="-1">Dislike</option></select>' + serverRatingMatchText + '</div></td>');
$('.MR_Rating[data-mapname="' + mapName + '"]').val(mapRatings[mapName].rating);
if (totalUsers === 0) { //new map...
$(this).find('span.likes').attr('title', 'New Map!').css({ 'width':'100%', 'background':'rebeccapurple', 'text-align':'center' }).text('Waiting for 100 Votes');
} else { //adjust the widths of the ratio bar with precision...
$(this).find('span.likes').width(likes_pc + '%');
$(this).find('span.indifferents').width(indifferents_pc + '%');
$(this).find('span.dislikes').width(dislikes_pc + '%');
}
}
});
//sort the table by last saved...
$(this).find('th:eq(' + GM_getValue('sortby', 1) + ')').trigger('click', true);
GM_setValue('mapRatings', mapRatings);
});
updateTotals();
$('.MR_Rating').on('change', function() {
mapRatings[this.dataset.mapname].rating = this.value;
mapRatings[this.dataset.mapname].lastRated = Date.now();
GM_setValue('mapRatings', mapRatings);
if ($(this).data('serverrating') !== null && +this.value === +$(this).data('serverrating')) {
$(this).next('.MR_RatingMatch').remove();
$(this).after(serverRatingsMatch);
} else {
$(this).next('.MR_RatingMatch').remove();
$(this).after(serverRatingsDontMatch);
}
if (+this.value === -1) {
$(this).prev('.MR_RatingText').remove();
$(this).before(serverRatingDislike);
} else if (+this.value === 0) {
$(this).prev('.MR_RatingText').remove();
$(this).before(serverRatingNeutral);
} else if (+this.value === 1) {
$(this).prev('.MR_RatingText').remove();
$(this).before(serverRatingLike);
} else {
$(this).prev('.MR_RatingText').remove();
$(this).before(serverRatingUnknown);
}
updateTotals();
});
//select the first row map as default...
$('#rotation').find('table.table tr:eq(1)').find('td:eq(0) a')[0].click();
updateMapDetails('#retired');
//make it a bit smaller so it fits better...
GM_addStyle('.table { width:90%; font-size:14px; margin:0 auto; }');
GM_addStyle('.table th { padding:2px 0.5em; user-select:none; cursor:pointer; }');
GM_addStyle('.table td { padding:2px 0.5em; text-shadow:1px 1px 1px #111; }');
GM_addStyle('.maps .ratio-bar { display:flex; border:1px solid #585858; height:14px; width:160px; font-size:10px; margin:2px 0 -1px; }');
GM_addStyle('table td:nth-child(1) { width: 140px; } ');
//row highlight on hover...
GM_addStyle('.table.table-stripped tbody tr:hover, .table.table-stripped tbody tr:nth-child(2n):hover { background:#131; }');
GM_addStyle('.MR_MapName { margin:0; max-width:140px !important; white-space:nowrap; overflow:hidden; }');
GM_addStyle('.MR_RatingContainer { display:flex; width:100px; }');
GM_addStyle('.MR_Rating { color:black; width:85px; }');
GM_addStyle('.MR_Totals { margin:5px; font-size:14px; font-weight:bold; text-align:center; }');
GM_addStyle('.MR_Totals_Label { margin:0 10px; font-size:12px; font-weight:normal; }');
GM_addStyle('.MR_RatingText { font-size:14px; width:13px; text-align:center; cursor:default; }');
GM_addStyle('.MR_RatingMatch { color:#d2d2d2; font-size:12px; width:15px; text-align:center; cursor:default; }');
GM_addStyle('.MR_Timeline { margin:0 0 5px 0; font-size:14px; font-weight:bold; text-align:center; }');
GM_addStyle('.MR_TimelineWin { display:inline-block; width:6px; height:6px; margin:0 1px 0 0; opacity:0.8; border:3px outset #0f0; }');
GM_addStyle('.MR_TimelineLoss { display:inline-block; width:6px; height:6px; margin:0 1px 0 0; opacity:0.8; border:3px outset #f00; }');
GM_addStyle('.MR_TimelineSSA { display:inline-block; width:6px; height:6px; margin:0 1px 0 0; opacity:0.8; background:#222; border:2px outset #aaff00; }');
GM_addStyle('.MR_TimelineUSA { display:inline-block; width:6px; height:6px; margin:0 1px 0 0; opacity:0.8; background:#222; border:2px outset #8c8c8c; }');
GM_addStyle('.MR_TimelineNewDay { display:inline-block; position:relative; width:1px; height:12px; opacity:0.8; background:none; border-right:1px solid #bbb; margin:0px 2px -3px 1px; }');
GM_addStyle('.MR_LastGameWin { width:6px; height:6px; margin-left:1px; opacity:0.8; border:3px outset #0f0; }');
GM_addStyle('.MR_LastGameLoss { width:6px; height:6px; margin-left:1px; opacity:0.8; border:3px outset #f00; }');
GM_addStyle('.MR_LastGameSSA { width:6px; height:6px; margin-left:1px; opacity:0.8; background:#222; border:2px outset #aaff00; }');
GM_addStyle('.MR_LastGameUSA { width:6px; height:6px; margin-left:1px; background:#222; border:2px outset #8c8c8c; }');
GM_addStyle('.MR_Divider { display:inline-block; padding:0 5px; font-size:14px; color:#666; }');
$('table').fadeIn(400);
$('.map-detail').fadeIn(400);
}); //getJSON
//show a large map preview...
$('body').on('click', '.preview', function() {
const src = $(this).attr('src').replace('-small', '');
let img = new Image();
$('body').append('<div id="MR_Large_Preview_Loading" style="position:fixed; top:50%; left:50%; width:240px; height:120px; line-height:120px; transform:translate(-50%, -50%); text-align:center; color:dodgerblue; background:#fff; border:1px solid red; border-radius:6px; box-shadow:0px 0px 25px 10px black; z-index:999;">Loading Image...</div>');
img.onload = function() {
$('#MR_Large_Preview').remove();
$('body').append('<div id="MR_Large_Preview" style="position:fixed; padding:10px; top:50%; left:50%; transform:translate(-50%, -50%); background:#111; border:1px solid red; border-radius:6px; box-shadow:0px 0px 25px 10px black; z-index:999;"></div>');
$('#MR_Large_Preview_Loading').remove();
$('#MR_Large_Preview').append(img).fadeIn(400);
};
img.onerror = function() {
$('#MR_Large_Preview_Loading').text('Could not load image.');
setTimeout(function() {
$('#MR_Large_Preview_Loading').remove();
}, 1800);
};
img.style.maxWidth = '90vw';
img.style.maxHeight = '90vh';
img.crossOrigin = 'anonymous';
img.src = src;
});
$('body').on('click', '#MR_Large_Preview', function() {
$(this).fadeOut(50, function() {
$(this).remove();
});
});
function updateTotals() {
$('.MR_Totals').remove();
$('.MR_Timeline').remove();
$('table').each(function() {
let $tbody = $(this).find('tbody');
let $rows = $tbody.find('tr');
let rotationType = capitalize($(this).closest('div.tab-pane').attr('id'));
let totals = { likes:0, neutrals:0, dislikes:0, plays:0, wins:0, losses:0 };
let timeline = [];
$rows.each(function(index) {
let mapName = $(this).find('.likes').prev().prev().text().trim();
if (!mapName) return false;
if (mapRatings[mapName].rating !== null && (+mapRatings[mapName].rating === 1 || +mapRatings[mapName].rating === -1 || +mapRatings[mapName].rating === 0)) {
let rating = +mapRatings[mapName].rating === 1 ? 'likes' : +mapRatings[mapName].rating === -1 ? 'dislikes' : 'neutrals';
totals[rating]++;
}
if (mapRatings[mapName].plays > 0) totals.plays += +mapRatings[mapName].plays;
if (mapRatings[mapName].wins > 0) totals.wins += +mapRatings[mapName].wins;
if (mapRatings[mapName].losses > 0) totals.losses += +mapRatings[mapName].losses;
if (showTimeline) {
if (mapRatings.hasOwnProperty(mapName) && mapRatings[mapName].lastResults) {
for (let i=0; i<mapRatings[mapName].lastResults.length; i++) {
if (mapRatings[mapName].lastResults[i].timePlayed) { //data format script version >= 0.0.7
timeline.push({ mapName:mapName, timePlayed:mapRatings[mapName].lastResults[i].timePlayed, result:mapRatings[mapName].lastResults[i].result, team:mapRatings[mapName].lastResults[i].team, scoreRed:mapRatings[mapName].lastResults[i].scoreRed, scoreBlue:mapRatings[mapName].lastResults[i].scoreBlue, saveAttempt:mapRatings[mapName].lastResults[i].saveAttempt });
} else if (mapRatings[mapName].lastPlayed && i === mapRatings[mapName].lastResults.length - 1) { //old data format (can only use the last game)
timeline.push( { mapName:mapName, timePlayed:mapRatings[mapName].lastPlayed, result:mapRatings[mapName].lastResults[i] } );
}
}
}
}
});
let score = ((totals.likes / (totals.likes + totals.dislikes) || 1) * 100).toFixed(2);
let total = (totals.likes + totals.dislikes + totals.neutrals) || 1;
$(this).after('<div class="MR_Totals">' +
'<span class="MR_Totals_Label"># Maps: ' + $rows.length + '</span>' +
'<span class="MR_Totals_Label" title="Score = Likes / (Likes + Dislikes)">My ' + rotationType + ' Score: ' + score + '%</span>' +
'<span class="MR_Totals_Label">' + serverRatingLike + 'Likes: ' + totals.likes + ' (' + (totals.likes / total * 100).toFixed(1) + '%)</span>' +
'<span class="MR_Totals_Label">' + serverRatingNeutral + 'Neutrals: ' + totals.neutrals + ' (' + (totals.neutrals / total * 100).toFixed(1) + '%)</span>' +
'<span class="MR_Totals_Label">' + serverRatingDislike + 'Dislikes: ' + totals.dislikes + ' (' + (totals.dislikes / total * 100).toFixed(1) + '%)</span>' +
(showMyPlays ? '<span class="MR_Totals_Label">Plays: ' + totals.plays + '</span>' : '') +
(showMyWins ? '<span class="MR_Totals_Label" title="' + totals.wins + ' Wins, ' + totals.losses + ' Losses">Win: ' + (totals.wins / ((totals.wins + totals.losses) || 1) * 100).toFixed(1) + '%</span>' : '') +
'<div style="font-weight:normal; font-style:italic; font-size:12px; margin:3px 0;">(Note: Map Ratings get updated to the server when you next play that map)</div></div>');
if (showTimeline && timeline.length) {
timeline.sort(function(a, b) {
return a.timePlayed - b.timePlayed;
});
const timelineGamesToShow = Math.min(timeline.length, timelineGames);
let games = '';
let wins = 0;
let capsFor = 0;
let capsAgainst = 0;
let redTotals = { count:0, wins:0, losses:0 };
let blueTotals = { count:0, wins:0, losses:0 };
let first = Math.max(0, timeline.length - timelineGamesToShow);
let lastDatePlayed = new Date(timeline[first].timePlayed).toDateString();
let lastDatePlayedPosition = first; // :)
for (let i=first; i<timeline.length; i++) {
if (timeline[i].timePlayed > 0) {
let thisGame = timeline[i];
let timePlayed = new Date(timeline[i].timePlayed);
let timePlayedDate = timePlayed.toDateString()
if (i > first && lastDatePlayed !== timePlayedDate) {
lastDatePlayed = timePlayedDate;
lastDatePlayedPosition = i;
games += '<span class="MR_TimelineNewDay" title="A new day begins..."></span>';
}
games += '<span class="' + (thisGame.result === 1 ? (thisGame.saveAttempt ? 'MR_TimelineSSA' : 'MR_TimelineWin') : (thisGame.saveAttempt ? 'MR_TimelineUSA' : 'MR_TimelineLoss')) + '" title="Map: ' + thisGame.mapName + (thisGame.mapAuthor ? '\nAuthor: ' + thisGame.mapAuthor : '') + '\nPlayed: ' + timePlayed.toDateString() + ', ' + timePlayed.toLocaleTimeString() + '\nResult: ' + (thisGame.team === 1 ? 'Red ' : 'Blue ') + (thisGame.team === 1 && thisGame.scoreRed > thisGame.scoreBlue || thisGame.team === 2 && thisGame.scoreBlue > thisGame.scoreRed ? 'Won ' : 'Lost ') + thisGame.scoreRed + ':' + thisGame.scoreBlue + (thisGame.saveAttempt ? ' (Save Attempt)' : '') + '"></span>';
if (timeline[i].result === 1) wins++;
if (timeline[i].scoreRed >= 0) {
capsFor += timeline[i].team === 1 ? timeline[i].scoreRed : timeline[i].scoreBlue;
capsAgainst += timeline[i].team === 1 ? timeline[i].scoreBlue : timeline[i].scoreRed;
if (timeline[i].team === 1) {
redTotals.count++;
if (timeline[i].result === 1) redTotals.wins++;
else if (!timeline[i].saveAttempt) redTotals.losses++;
} else {
blueTotals.count++;
if (timeline[i].result === 1) blueTotals.wins++;
else if (!timeline[i].saveAttempt) blueTotals.losses++;
}
}
}
}
let todayWins = 0, todayGames = 0;
let todayLabel = 'Last Day: ';
for (let i=lastDatePlayedPosition; i<timeline.length; i++) {
if (timeline[i].timePlayed > 0) {
if (timeline[i].result === 1) todayWins++;
if (timeline[i].result === 1 || (timeline[i].result === 0 && !timeline[i].saveAttempt)) todayGames++;
}
}
let lastDay = new Date(timeline[lastDatePlayedPosition].timePlayed);
let today = new Date();
let divider = '<span class="MR_Divider">|</span>';
if (lastDay.toDateString() === today.toDateString()) todayLabel = 'Today: ';
else if (lastDay.getFullYear() === today.getFullYear() && lastDay.getMonth() === today.getMonth() && lastDay.getDate() === today.getDate() - 1) todayLabel = 'Yesterday: ';
$(this).before('<div class="MR_Timeline"><div style="margin:-4px;"><span title="' + timeline.length + ' available">Last ' + timelineGamesToShow + ' Games (for this rotation)</span></div><div>' + games + '</div><div style="font-size:12px;">Win: ' + ((wins / timelineGamesToShow) * 100).toFixed(2) + '%' + divider + 'Caps F/A: ' + capsFor + '/' + capsAgainst + divider + 'Red W%: ' + ((redTotals.wins / (redTotals.wins + redTotals.losses)) * 100).toFixed(0) + '% (' + redTotals.count + ' games)' + divider + 'Blue W%: ' + ((blueTotals.wins / (blueTotals.wins + blueTotals.losses)) * 100).toFixed(0) + '% (' + blueTotals.count + ' games)' + divider + '' + todayLabel + ((todayWins / todayGames) * 100).toFixed(2) + '% </div></div>');
}
});
}
function addLastResults(row, mapName) {
if (!mapRatings[mapName].hasOwnProperty('lastResults') || !mapRatings[mapName].lastResults.length) {
return;
}
let divid = 'MR_Results_' + mapName.replace(/[^a-z0-9]/gi, '');
$(row).find('.lastResult').append('<div id="' + divid + '" style="display:flex; flex-flow:row nowrap; justify-content:center;"></div>');
for (let i = 0; i < mapRatings[mapName].lastResults.length; i++) {
if (mapRatings[mapName].lastResults[i].hasOwnProperty('result')) {
let thisGame = mapRatings[mapName].lastResults[i];
let timePlayed = new Date(thisGame.timePlayed);
$('#' + divid).append('<span class="' + (thisGame.result === 1 ? (thisGame.saveAttempt ? 'MR_LastGameSSA' : 'MR_LastGameWin') : (thisGame.saveAttempt ? 'MR_LastGameUSA' : 'MR_LastGameLoss')) + '" title="Map: ' + mapName + (thisGame.mapAuthor ? '\nAuthor: ' + thisGame.mapAuthor : '') + '\nPlayed: ' + timePlayed.toDateString() + ', ' + timePlayed.toLocaleTimeString() + '\nResult: ' + (thisGame.team === 1 ? 'Red ' : 'Blue ') + (thisGame.team === 1 && thisGame.scoreRed > thisGame.scoreBlue || thisGame.team === 2 && thisGame.scoreBlue > thisGame.scoreRed ? 'Won ' : 'Lost ') + thisGame.scoreRed + ':' + thisGame.scoreBlue + (thisGame.saveAttempt ? ' (Save Attempt)' : '') + '"></span>');
} else {
$('#' + divid).append('<span class="' + (mapRatings[mapName].lastResults[i] === 1 ? 'MR_LastGameWin' : 'MR_LastGameLoss') + '" title="' + (mapRatings[mapName].lastResults[i] === 1 ? 'Win' : 'Loss') + '"></span>');
}
}
}
function updateLastPlayed() {
$('table').each(function() {
let $tbody = $(this).find('tbody');
let $rows = $tbody.find('tr');
$rows.each(function(index) {
let mapName = $(this).find('.likes').prev().prev().text().trim();
$(this).find('td.lastPlayed').text(mapRatings[mapName].lastPlayed ? dayjs(mapRatings[mapName].lastPlayed).from() : '-');
});
});
}
if (showMyLastPlayed) {
setInterval(updateLastPlayed, 60000); //so "5 minutes ago" is always accurate
}
function updateMapDetails(currentTab='#rotation') {
let mapDetail = $(currentTab).find('.map-detail');
mapDetail.find('.likes').hide(0);
mapDetail.find('.indifferents').hide(0);
mapDetail.find('.dislikes').hide(0);
mapDetail.find('.percent').hide(0);
mapDetail.find('.plays').hide(0);
mapDetail.find('.votes').hide(0);
setTimeout(function() {
let likes = parseInt( mapDetail.find('.likes').text().replace(/,/g, ''), 10 );
let indifferents = parseInt( mapDetail.find('.indifferents').text().replace(/,/g, ''), 10 );
let dislikes = parseInt( mapDetail.find('.dislikes').text().replace(/,/g, ''), 10 );
let totalUsers = likes + indifferents + dislikes;
let score = likes / ((likes + dislikes) || 1) * 100;
mapDetail.find('.likes').html((likes/(totalUsers||1)*100).toFixed(1)+'%').attr('title', likes).fadeIn(100);
mapDetail.find('.indifferents').text((indifferents/(totalUsers||1)*100).toFixed(1)+'%').attr('title', indifferents).fadeIn(100);
mapDetail.find('.dislikes').text((dislikes/(totalUsers||1)*100).toFixed(1)+'%').attr('title', dislikes).fadeIn(100);
mapDetail.find('.percent').text((score).toFixed(1)+'%').attr('title', 'Score = Likes / (Likes + Dislikes)').fadeIn(100);
if (totalUsers === 0) {
mapDetail.find('.plays').text('0');
mapDetail.find('.votes').text('0');
}
mapDetail.find('.plays').fadeIn(100);
mapDetail.find('.votes').fadeIn(100);
}, 300);
}
//helpers...
function comparer(index) {
return function(a, b) {
let valA = $(a).children('td').eq(index).data('sortby');
let valB = $(b).children('td').eq(index).data('sortby');
if ($.isNumeric(valA) && $.isNumeric(valB)) {
if (valA === valB) {
let valA2 = $(a).children('td').eq(index).data('sortby2');
let valB2 = $(b).children('td').eq(index).data('sortby2');
if (valB2 && valA2) return valB2 - valA2;
}
return valB - valA;
} else {
valA.localeCompare(valB);
}
};
}
function capitalize(value) {
return value.charAt(0).toUpperCase() + value.slice(1);
}
}
});
//dayjs (and the dayjs "relativeTime" plugin)
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.dayjs=e()}(this,function(){"use strict";var t="millisecond",e="second",n="minute",r="hour",i="day",s="week",u="month",a="quarter",o="year",f="date",h=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[^0-9]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,c=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,d={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_")},$=function(t,e,n){var r=String(t);return!r||r.length>=e?t:""+Array(e+1-r.length).join(n)+t},l={s:$,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),i=n%60;return(e<=0?"+":"-")+$(r,2,"0")+":"+$(i,2,"0")},m:function t(e,n){if(e.date()<n.date())return-t(n,e);var r=12*(n.year()-e.year())+(n.month()-e.month()),i=e.clone().add(r,u),s=n-i<0,a=e.clone().add(r+(s?-1:1),u);return+(-(r+(n-i)/(s?i-a:a-i))||0)},a:function(t){return t<0?Math.ceil(t)||0:Math.floor(t)},p:function(h){return{M:u,y:o,w:s,d:i,D:f,h:r,m:n,s:e,ms:t,Q:a}[h]||String(h||"").toLowerCase().replace(/s$/,"")},u:function(t){return void 0===t}},y="en",M={};M[y]=d;var m=function(t){return t instanceof S},D=function(t,e,n){var r;if(!t)return y;if("string"==typeof t)M[t]&&(r=t),e&&(M[t]=e,r=t);else{var i=t.name;M[i]=t,r=i}return!n&&r&&(y=r),r||!n&&y},v=function(t,e){if(m(t))return t.clone();var n="object"==typeof e?e:{};return n.date=t,n.args=arguments,new S(n)},g=l;g.l=D,g.i=m,g.w=function(t,e){return v(t,{locale:e.$L,utc:e.$u,x:e.$x,$offset:e.$offset})};var S=function(){function d(t){this.$L=D(t.locale,null,!0),this.parse(t)}var $=d.prototype;return $.parse=function(t){this.$d=function(t){var e=t.date,n=t.utc;if(null===e)return new Date(NaN);if(g.u(e))return new Date;if(e instanceof Date)return new Date(e);if("string"==typeof e&&!/Z$/i.test(e)){var r=e.match(h);if(r){var i=r[2]-1||0,s=(r[7]||"0").substring(0,3);return n?new Date(Date.UTC(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)):new Date(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)}}return new Date(e)}(t),this.$x=t.x||{},this.init()},$.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},$.$utils=function(){return g},$.isValid=function(){return!("Invalid Date"===this.$d.toString())},$.isSame=function(t,e){var n=v(t);return this.startOf(e)<=n&&n<=this.endOf(e)},$.isAfter=function(t,e){return v(t)<this.startOf(e)},$.isBefore=function(t,e){return this.endOf(e)<v(t)},$.$g=function(t,e,n){return g.u(t)?this[e]:this.set(n,t)},$.unix=function(){return Math.floor(this.valueOf()/1e3)},$.valueOf=function(){return this.$d.getTime()},$.startOf=function(t,a){var h=this,c=!!g.u(a)||a,d=g.p(t),$=function(t,e){var n=g.w(h.$u?Date.UTC(h.$y,e,t):new Date(h.$y,e,t),h);return c?n:n.endOf(i)},l=function(t,e){return g.w(h.toDate()[t].apply(h.toDate("s"),(c?[0,0,0,0]:[23,59,59,999]).slice(e)),h)},y=this.$W,M=this.$M,m=this.$D,D="set"+(this.$u?"UTC":"");switch(d){case o:return c?$(1,0):$(31,11);case u:return c?$(1,M):$(0,M+1);case s:var v=this.$locale().weekStart||0,S=(y<v?y+7:y)-v;return $(c?m-S:m+(6-S),M);case i:case f:return l(D+"Hours",0);case r:return l(D+"Minutes",1);case n:return l(D+"Seconds",2);case e:return l(D+"Milliseconds",3);default:return this.clone()}},$.endOf=function(t){return this.startOf(t,!1)},$.$set=function(s,a){var h,c=g.p(s),d="set"+(this.$u?"UTC":""),$=(h={},h[i]=d+"Date",h[f]=d+"Date",h[u]=d+"Month",h[o]=d+"FullYear",h[r]=d+"Hours",h[n]=d+"Minutes",h[e]=d+"Seconds",h[t]=d+"Milliseconds",h)[c],l=c===i?this.$D+(a-this.$W):a;if(c===u||c===o){var y=this.clone().set(f,1);y.$d[$](l),y.init(),this.$d=y.set(f,Math.min(this.$D,y.daysInMonth())).$d}else $&&this.$d[$](l);return this.init(),this},$.set=function(t,e){return this.clone().$set(t,e)},$.get=function(t){return this[g.p(t)]()},$.add=function(t,a){var f,h=this;t=Number(t);var c=g.p(a),d=function(e){var n=v(h);return g.w(n.date(n.date()+Math.round(e*t)),h)};if(c===u)return this.set(u,this.$M+t);if(c===o)return this.set(o,this.$y+t);if(c===i)return d(1);if(c===s)return d(7);var $=(f={},f[n]=6e4,f[r]=36e5,f[e]=1e3,f)[c]||1,l=this.$d.getTime()+t*$;return g.w(l,this)},$.subtract=function(t,e){return this.add(-1*t,e)},$.format=function(t){var e=this;if(!this.isValid())return"Invalid Date";var n=t||"YYYY-MM-DDTHH:mm:ssZ",r=g.z(this),i=this.$locale(),s=this.$H,u=this.$m,a=this.$M,o=i.weekdays,f=i.months,h=function(t,r,i,s){return t&&(t[r]||t(e,n))||i[r].substr(0,s)},d=function(t){return g.s(s%12||12,t,"0")},$=i.meridiem||function(t,e,n){var r=t<12?"AM":"PM";return n?r.toLowerCase():r},l={YY:String(this.$y).slice(-2),YYYY:this.$y,M:a+1,MM:g.s(a+1,2,"0"),MMM:h(i.monthsShort,a,f,3),MMMM:h(f,a),D:this.$D,DD:g.s(this.$D,2,"0"),d:String(this.$W),dd:h(i.weekdaysMin,this.$W,o,2),ddd:h(i.weekdaysShort,this.$W,o,3),dddd:o[this.$W],H:String(s),HH:g.s(s,2,"0"),h:d(1),hh:d(2),a:$(s,u,!0),A:$(s,u,!1),m:String(u),mm:g.s(u,2,"0"),s:String(this.$s),ss:g.s(this.$s,2,"0"),SSS:g.s(this.$ms,3,"0"),Z:r};return n.replace(c,function(t,e){return e||l[t]||r.replace(":","")})},$.utcOffset=function(){return 15*-Math.round(this.$d.getTimezoneOffset()/15)},$.diff=function(t,f,h){var c,d=g.p(f),$=v(t),l=6e4*($.utcOffset()-this.utcOffset()),y=this-$,M=g.m(this,$);return M=(c={},c[o]=M/12,c[u]=M,c[a]=M/3,c[s]=(y-l)/6048e5,c[i]=(y-l)/864e5,c[r]=y/36e5,c[n]=y/6e4,c[e]=y/1e3,c)[d]||y,h?M:g.a(M)},$.daysInMonth=function(){return this.endOf(u).$D},$.$locale=function(){return M[this.$L]},$.locale=function(t,e){if(!t)return this.$L;var n=this.clone(),r=D(t,e,!0);return r&&(n.$L=r),n},$.clone=function(){return g.w(this.$d,this)},$.toDate=function(){return new Date(this.valueOf())},$.toJSON=function(){return this.isValid()?this.toISOString():null},$.toISOString=function(){return this.$d.toISOString()},$.toString=function(){return this.$d.toUTCString()},d}(),p=S.prototype;return v.prototype=p,[["$ms",t],["$s",e],["$m",n],["$H",r],["$W",i],["$M",u],["$y",o],["$D",f]].forEach(function(t){p[t[1]]=function(e){return this.$g(e,t[0],t[1])}}),v.extend=function(t,e){return t.$i||(t(e,S,v),t.$i=!0),v},v.locale=D,v.isDayjs=m,v.unix=function(t){return v(1e3*t)},v.en=M[y],v.Ls=M,v.p={},v});
!function(r,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):r.dayjs_plugin_relativeTime=t()}(this,function(){"use strict";return function(r,t,e){r=r||{};var n=t.prototype,o={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"};function i(r,t,e,o){return n.fromToBase(r,t,e,o)}e.en.relativeTime=o,n.fromToBase=function(t,n,i,d,u){for(var a,f,s,l=i.$locale().relativeTime||o,h=r.thresholds||[{l:"s",r:44,d:"second"},{l:"m",r:89},{l:"mm",r:44,d:"minute"},{l:"h",r:89},{l:"hh",r:21,d:"hour"},{l:"d",r:35},{l:"dd",r:25,d:"day"},{l:"M",r:45},{l:"MM",r:10,d:"month"},{l:"y",r:17},{l:"yy",d:"year"}],m=h.length,c=0;c<m;c+=1){var y=h[c];y.d&&(a=d?e(t).diff(i,y.d,!0):i.diff(t,y.d,!0));var p=(r.rounding||Math.round)(Math.abs(a));if(s=a>0,p<=y.r||!y.r){p<=1&&c>0&&(y=h[c-1]);var v=l[y.l];u&&(p=u(""+p)),f="string"==typeof v?v.replace("%d",p):v(p,n,y.l,s);break}}if(n)return f;var M=s?l.future:l.past;return"function"==typeof M?M(f):M.replace("%s",f)},n.to=function(r,t){return i(r,t,this,!0)},n.from=function(r,t){return i(r,t,this)};var d=function(r){return r.$u?e.utc():e()};n.toNow=function(r){return this.to(d(this),r)},n.fromNow=function(r){return this.from(d(this),r)}}});
dayjs.extend(dayjs_plugin_relativeTime);
@zmunro
Copy link

zmunro commented Jun 10, 2022

I think line 520 should initialize lastDatePlayedPosition to first like:
let lastDatePlayedPosition = first;
I was getting an error trying to run this script before that. The error was for the line:

let lastDay = new Date(timeline[lastDatePlayedPosition].timePlayed);

It said that it can't find property timePlayed of undefined. Once I made sure that lastDatePlayedPosition was initialized to first to begin with though, it worked fine as far as I can tell. @nabbynz

@nabbynz
Copy link
Author

nabbynz commented Jun 11, 2022

I think line 520 should initialize lastDatePlayedPosition to first like: let lastDatePlayedPosition = first;

Updated, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment