From 9323f7d892b02c41dea094fc69ae8532b7836189 Mon Sep 17 00:00:00 2001 From: Remy Jette Date: Tue, 29 Aug 2023 14:58:49 -0700 Subject: [PATCH] WebHost: Add a summary row to the Multiworld Tracker (#1965) * WebHost: Add a summary row to the Multiworld Tracker Implements suggestions from the generation-suggestions channel: - https://discord.com/channels/731205301247803413/1124186131911688262 - https://discord.com/channels/731205301247803413/1109513647274856518 * Improve secondsToHours function, and remove jQuery from footerCallback function. * Don't show the summary row on game-specific multi trackers --------- Co-authored-by: Chris Wilson --- WebHostLib/static/assets/trackerCommon.js | 35 +++++++++++++++++++---- WebHostLib/static/styles/tracker.css | 12 +++++--- WebHostLib/templates/multiTracker.html | 15 +++++++++- WebHostLib/tracker.py | 10 ++++++- 4 files changed, 60 insertions(+), 12 deletions(-) diff --git a/WebHostLib/static/assets/trackerCommon.js b/WebHostLib/static/assets/trackerCommon.js index c08590cb..41c4020d 100644 --- a/WebHostLib/static/assets/trackerCommon.js +++ b/WebHostLib/static/assets/trackerCommon.js @@ -14,6 +14,17 @@ const adjustTableHeight = () => { } }; +/** + * Convert an integer number of seconds into a human readable HH:MM format + * @param {Number} seconds + * @returns {string} + */ +const secondsToHours = (seconds) => { + let hours = Math.floor(seconds / 3600); + let minutes = Math.floor((seconds - (hours * 3600)) / 60).toString().padStart(2, '0'); + return `${hours}:${minutes}`; +}; + window.addEventListener('load', () => { const tables = $(".table").DataTable({ paging: false, @@ -27,7 +38,18 @@ window.addEventListener('load', () => { stateLoadCallback: function(settings) { return JSON.parse(localStorage.getItem(`DataTables_${settings.sInstance}_/tracker`)); }, + footerCallback: function(tfoot, data, start, end, display) { + if (tfoot) { + const activityData = this.api().column('lastActivity:name').data().toArray().filter(x => !isNaN(x)); + Array.from(tfoot?.children).find(td => td.classList.contains('last-activity')).innerText = + (activityData.length) ? secondsToHours(Math.min(...activityData)) : 'None'; + } + }, columnDefs: [ + { + targets: 'last-activity', + name: 'lastActivity' + }, { targets: 'hours', render: function (data, type, row) { @@ -40,11 +62,7 @@ window.addEventListener('load', () => { if (data === "None") return data; - let hours = Math.floor(data / 3600); - let minutes = Math.floor((data - (hours * 3600)) / 60); - - if (minutes < 10) {minutes = "0"+minutes;} - return hours+':'+minutes; + return secondsToHours(data); } }, { @@ -114,11 +132,16 @@ window.addEventListener('load', () => { if (status === "success") { target.find(".table").each(function (i, new_table) { const new_trs = $(new_table).find("tbody>tr"); + const footer_tr = $(new_table).find("tfoot>tr"); const old_table = tables.eq(i); const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop(); const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft(); old_table.clear(); - old_table.rows.add(new_trs).draw(); + if (footer_tr.length) { + $(old_table.table).find("tfoot").html(footer_tr); + } + old_table.rows.add(new_trs); + old_table.draw(); $(old_table.settings()[0].nScrollBody).scrollTop(topscroll); $(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll); }); diff --git a/WebHostLib/static/styles/tracker.css b/WebHostLib/static/styles/tracker.css index 0e00553c..0cc2ede5 100644 --- a/WebHostLib/static/styles/tracker.css +++ b/WebHostLib/static/styles/tracker.css @@ -55,16 +55,16 @@ table.dataTable thead{ font-family: LexendDeca-Regular, sans-serif; } -table.dataTable tbody{ +table.dataTable tbody, table.dataTable tfoot{ background-color: #dce2bd; font-family: LexendDeca-Light, sans-serif; } -table.dataTable tbody tr:hover{ +table.dataTable tbody tr:hover, table.dataTable tfoot tr:hover{ background-color: #e2eabb; } -table.dataTable tbody td{ +table.dataTable tbody td, table.dataTable tfoot td{ padding: 4px 6px; } @@ -97,10 +97,14 @@ table.dataTable thead th.lower-row{ top: 46px; } -table.dataTable tbody td{ +table.dataTable tbody td, table.dataTable tfoot td{ border: 1px solid #bba967; } +table.dataTable tfoot td{ + font-weight: bold; +} + div.dataTables_scrollBody{ background-color: inherit !important; } diff --git a/WebHostLib/templates/multiTracker.html b/WebHostLib/templates/multiTracker.html index 2232cd0f..40d89eb4 100644 --- a/WebHostLib/templates/multiTracker.html +++ b/WebHostLib/templates/multiTracker.html @@ -37,7 +37,7 @@ {% endblock %} Checks % - Last
Activity + Last
Activity @@ -64,6 +64,19 @@ {%- endfor -%} + {% if not self.custom_table_headers() | trim %} + + + + Total + All Games + {{ completed_worlds }}/{{ players|length }} Complete + {{ players.values()|sum(attribute='Total') }}/{{ total_locations[team] }} + {{ (players.values()|sum(attribute='Total') / total_locations[team] * 100) | int }} + + + + {% endif %} {% endfor %} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index d3fd0fb0..80ccc720 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -1366,6 +1366,10 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s for playernumber in range(1, len(team) + 1) if playernumber not in groups} for teamnumber, team in enumerate(names)} + total_locations = {teamnumber: sum(len(locations[playernumber]) + for playernumber in range(1, len(team) + 1) if playernumber not in groups) + for teamnumber, team in enumerate(names)} + hints = {team: set() for team in range(len(names))} if room.multisave: multisave = restricted_loads(room.multisave) @@ -1390,11 +1394,14 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp) player_names = {} + completed_worlds = 0 states: typing.Dict[typing.Tuple[int, int], int] = {} for team, names in enumerate(names): for player, name in enumerate(names, 1): player_names[team, player] = name states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0) + if states[team, player] == 30: # Goal Completed + completed_worlds += 1 long_player_names = player_names.copy() for (team, player), alias in multisave.get("name_aliases", {}).items(): player_names[team, player] = alias @@ -1410,7 +1417,8 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s activity_timers=activity_timers, video=video, hints=hints, long_player_names=long_player_names, multisave=multisave, precollected_items=precollected_items, groups=groups, - locations=locations, games=games, states=states, + locations=locations, total_locations=total_locations, games=games, states=states, + completed_worlds=completed_worlds, custom_locations=custom_locations, custom_items=custom_items, )