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 <chris@legendserver.info>
This commit is contained in:
Remy Jette 2023-08-29 14:58:49 -07:00 committed by GitHub
parent 30e747bb4c
commit 9323f7d892
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 60 additions and 12 deletions

View File

@ -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', () => { window.addEventListener('load', () => {
const tables = $(".table").DataTable({ const tables = $(".table").DataTable({
paging: false, paging: false,
@ -27,7 +38,18 @@ window.addEventListener('load', () => {
stateLoadCallback: function(settings) { stateLoadCallback: function(settings) {
return JSON.parse(localStorage.getItem(`DataTables_${settings.sInstance}_/tracker`)); 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: [ columnDefs: [
{
targets: 'last-activity',
name: 'lastActivity'
},
{ {
targets: 'hours', targets: 'hours',
render: function (data, type, row) { render: function (data, type, row) {
@ -40,11 +62,7 @@ window.addEventListener('load', () => {
if (data === "None") if (data === "None")
return data; return data;
let hours = Math.floor(data / 3600); return secondsToHours(data);
let minutes = Math.floor((data - (hours * 3600)) / 60);
if (minutes < 10) {minutes = "0"+minutes;}
return hours+':'+minutes;
} }
}, },
{ {
@ -114,11 +132,16 @@ window.addEventListener('load', () => {
if (status === "success") { if (status === "success") {
target.find(".table").each(function (i, new_table) { target.find(".table").each(function (i, new_table) {
const new_trs = $(new_table).find("tbody>tr"); const new_trs = $(new_table).find("tbody>tr");
const footer_tr = $(new_table).find("tfoot>tr");
const old_table = tables.eq(i); const old_table = tables.eq(i);
const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop(); const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop();
const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft(); const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft();
old_table.clear(); 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).scrollTop(topscroll);
$(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll); $(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll);
}); });

View File

@ -55,16 +55,16 @@ table.dataTable thead{
font-family: LexendDeca-Regular, sans-serif; font-family: LexendDeca-Regular, sans-serif;
} }
table.dataTable tbody{ table.dataTable tbody, table.dataTable tfoot{
background-color: #dce2bd; background-color: #dce2bd;
font-family: LexendDeca-Light, sans-serif; font-family: LexendDeca-Light, sans-serif;
} }
table.dataTable tbody tr:hover{ table.dataTable tbody tr:hover, table.dataTable tfoot tr:hover{
background-color: #e2eabb; background-color: #e2eabb;
} }
table.dataTable tbody td{ table.dataTable tbody td, table.dataTable tfoot td{
padding: 4px 6px; padding: 4px 6px;
} }
@ -97,10 +97,14 @@ table.dataTable thead th.lower-row{
top: 46px; top: 46px;
} }
table.dataTable tbody td{ table.dataTable tbody td, table.dataTable tfoot td{
border: 1px solid #bba967; border: 1px solid #bba967;
} }
table.dataTable tfoot td{
font-weight: bold;
}
div.dataTables_scrollBody{ div.dataTables_scrollBody{
background-color: inherit !important; background-color: inherit !important;
} }

View File

@ -37,7 +37,7 @@
{% endblock %} {% endblock %}
<th class="center-column">Checks</th> <th class="center-column">Checks</th>
<th class="center-column">&percnt;</th> <th class="center-column">&percnt;</th>
<th class="center-column hours">Last<br>Activity</th> <th class="center-column hours last-activity">Last<br>Activity</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -64,6 +64,19 @@
</tr> </tr>
{%- endfor -%} {%- endfor -%}
</tbody> </tbody>
{% if not self.custom_table_headers() | trim %}
<tfoot>
<tr>
<td></td>
<td>Total</td>
<td>All Games</td>
<td>{{ completed_worlds }}/{{ players|length }} Complete</td>
<td class="center-column">{{ players.values()|sum(attribute='Total') }}/{{ total_locations[team] }}</td>
<td class="center-column">{{ (players.values()|sum(attribute='Total') / total_locations[team] * 100) | int }}</td>
<td class="center-column last-activity"></td>
</tr>
</tfoot>
{% endif %}
</table> </table>
</div> </div>
{% endfor %} {% endfor %}

View File

@ -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 playernumber in range(1, len(team) + 1) if playernumber not in groups}
for teamnumber, team in enumerate(names)} 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))} hints = {team: set() for team in range(len(names))}
if room.multisave: if room.multisave:
multisave = restricted_loads(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) activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
player_names = {} player_names = {}
completed_worlds = 0
states: typing.Dict[typing.Tuple[int, int], int] = {} states: typing.Dict[typing.Tuple[int, int], int] = {}
for team, names in enumerate(names): for team, names in enumerate(names):
for player, name in enumerate(names, 1): for player, name in enumerate(names, 1):
player_names[team, player] = name player_names[team, player] = name
states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0) 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() long_player_names = player_names.copy()
for (team, player), alias in multisave.get("name_aliases", {}).items(): for (team, player), alias in multisave.get("name_aliases", {}).items():
player_names[team, player] = alias 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, activity_timers=activity_timers, video=video, hints=hints,
long_player_names=long_player_names, long_player_names=long_player_names,
multisave=multisave, precollected_items=precollected_items, groups=groups, 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, custom_locations=custom_locations, custom_items=custom_items,
) )