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:
parent
30e747bb4c
commit
9323f7d892
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<th class="center-column">Checks</th>
|
<th class="center-column">Checks</th>
|
||||||
<th class="center-column">%</th>
|
<th class="center-column">%</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 %}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue