Add own-body spectator camera and player names; record Workshop 0.0.139
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-11-03 21:16:05 +01:00
parent 4f02ad64b4
commit 828bb0a1a7
17 changed files with 1048 additions and 35 deletions
@@ -132,9 +132,9 @@ export function dashboardApp() {
return stack(number(p[key]), `${number(p[coverage])}/${number(p.attempts)} missions reported this counter`);
}
function renderPlayers(data) {
const top = toolbar('Player records', 'Authenticated participants in completed gameplay missions. Names are available in individual mission reports.');
const rows = data.players.map(p => [el('span', p.playerId, 'mono'), number(p.attempts), stack(number(p.escapes), `${number(p.didNotEscape)} did not escape`), duration(p.bestEscapeSeconds), counter(p, 'kills', 'missionsWithKills'), counter(p, 'friendlyKills', 'missionsWithFriendlyKills'), counter(p, 'deaths', 'missionsWithDeaths'), counter(p, 'downed', 'missionsWithDowned'), counter(p, 'recoveries', 'missionsWithRecoveries'), `${number(p.partialMissions)} partial / ${number(p.truncatedMissions)} truncated`]);
$('content').replaceChildren(top, table(['Player ID', 'Attempts', 'Escapes', 'Best escape', 'Kills', 'Friendly kills', 'Deaths', 'Downed', 'Recoveries', 'Coverage'], rows), el('p', 'Counter coverage means the field was present, not that a kill or injury happened. A dash means no recorded value.', 'small muted'), pager(data.nextAfter));
const top = toolbar('Player records', 'Authenticated participants in completed gameplay missions. Most recent recorded names; IDs remain the stable identity.');
const rows = data.players.map(p => [stack(p.name || 'Unknown player', p.playerId, true), number(p.attempts), stack(number(p.escapes), `${number(p.didNotEscape)} did not escape`), duration(p.bestEscapeSeconds), counter(p, 'kills', 'missionsWithKills'), counter(p, 'friendlyKills', 'missionsWithFriendlyKills'), counter(p, 'deaths', 'missionsWithDeaths'), counter(p, 'downed', 'missionsWithDowned'), counter(p, 'recoveries', 'missionsWithRecoveries'), `${number(p.partialMissions)} partial / ${number(p.truncatedMissions)} truncated`]);
$('content').replaceChildren(top, table(['Player / ID', 'Attempts', 'Escapes', 'Best escape', 'Kills', 'Friendly kills', 'Deaths', 'Downed', 'Recoveries', 'Coverage'], rows), el('p', 'Counter coverage means the field was present, not that a kill or injury happened. A dash means no recorded value.', 'small muted'), pager(data.nextAfter));
}
async function refresh() {
controller?.abort(); const run = new AbortController(); controller = run; $('refresh').disabled = true;
+10 -4
View File
@@ -226,9 +226,15 @@ async function players(request, env, url) {
const after = url.searchParams.get('after');
if (after !== null) id(after);
const conditions = [], values = [];
if (serverId) { conditions.push('server_id = ?'); values.push(serverId); }
if (after) { conditions.push('player_id > ?'); values.push(after.toLowerCase()); }
const rows = await env.DB.prepare(`SELECT player_id AS playerId, COUNT(*) AS attempts,
if (serverId) { conditions.push('f.server_id = ?'); values.push(serverId); }
if (after) { conditions.push('f.player_id > ?'); values.push(after.toLowerCase()); }
const rows = await env.DB.prepare(`WITH ranked AS (
SELECT f.*, ROW_NUMBER() OVER (PARTITION BY f.player_id
ORDER BY (trim(f.name) != '') DESC, m.occurred_at DESC, m.id DESC) AS nameRank
FROM final_players f JOIN missions m ON m.server_id = f.server_id AND m.mission_id = f.mission_id
${conditions.length ? `WHERE ${conditions.join(' AND ')}` : ''}
) SELECT player_id AS playerId,
MAX(CASE WHEN nameRank = 1 THEN NULLIF(trim(name), '') END) AS name, COUNT(*) AS attempts,
SUM(escaped) AS escapes, SUM(1 - escaped) AS didNotEscape,
SUM(partial) AS partialMissions, SUM(players_truncated) AS truncatedMissions,
MIN(CASE WHEN escaped = 1 THEN elapsed_seconds END) AS bestEscapeSeconds,
@@ -237,7 +243,7 @@ async function players(request, env, url) {
SUM(deaths) AS deaths, COUNT(deaths) AS missionsWithDeaths,
SUM(downed) AS downed, COUNT(downed) AS missionsWithDowned,
SUM(recoveries) AS recoveries, COUNT(recoveries) AS missionsWithRecoveries
FROM final_players ${conditions.length ? `WHERE ${conditions.join(' AND ')}` : ''}
FROM ranked
GROUP BY player_id ORDER BY player_id LIMIT ?`).bind(...values, limit).all();
return json({players: rows.results, nextAfter: rows.results.length === limit ? rows.results.at(-1).playerId : null});
}