better umm.yeah

This commit is contained in:
legop3
2026-06-19 20:18:36 -04:00
parent eec009c485
commit 36bf5826b9
11 changed files with 241 additions and 51 deletions
@@ -258,17 +258,16 @@ function getPublicState(rawState, context = {}) {
const participantResults = state.status === 'ended' && state.finalResult
? state.finalResult.participants
: getParticipantList(state);
const scannedObjects = Object.values(state.seenObjects || {})
.sort((a, b) => (a.firstScannedAt || 0) - (b.firstScannedAt || 0))
.slice(-5)
.map((object) => object.label || object.entityId || object.code)
.filter(Boolean);
return {
id: GAME_ID,
title: 'Most items',
status: state.status,
headline: state.lastMessage,
detail: state.status === 'running'
? `${uniqueItems} unique items, ${Math.ceil(remainingMs / 1000)} seconds left`
: state.finalResult
? `${state.finalResult.uniqueItems} unique items in last round`
: 'scan different known objects',
uniqueItems,
totalObjectScans: state.totalObjectScans,
duplicateObjectScans: state.duplicateObjectScans,
@@ -304,6 +303,25 @@ function getPublicState(rawState, context = {}) {
{ label: 'Object scans', value: state.totalObjectScans },
{ label: 'World record', value: worldRecordText },
],
sections: [
{
title: 'Progress',
// This replaces the older top-level detail text with a display-owned
// section so every client renders game explanations from the same
// structured field instead of guessing between headline/detail.
items: [
state.status === 'running'
? `${uniqueItems} unique items, ${Math.ceil(remainingMs / 1000)} seconds left`
: state.finalResult
? `${state.finalResult.uniqueItems} unique items in last round`
: 'scan different known objects',
],
},
{
title: 'Recent items',
items: scannedObjects.length ? scannedObjects : ['no objects scanned yet'],
},
],
results: Object.values(state.seenObjects || {}).slice(-3).map((object) => ({
label: 'Item',
value: object.label || object.entityId || object.code,
@@ -273,15 +273,24 @@ function getPublicState(rawState, context = {}) {
const currentStep = state.currentQuest?.steps?.[state.progressIndex] || null;
const totalSteps = state.currentQuest?.steps?.length || 0;
const stepLabel = totalSteps ? `Item ${Math.min(state.progressIndex + 1, totalSteps)} of ${totalSteps}` : 'Find the object';
const orderedList = state.currentQuest?.steps?.length
? state.currentQuest.steps.map((step, idx) => `${idx + 1}. ${step.label}`).join(' -> ')
: 'add object barcodes to the registry';
const routeItems = state.currentQuest?.steps?.length
? state.currentQuest.steps.map((step, idx) => ({
// The item label is already numbered here because the same payload is
// shown on both the far-away scanner screen and the compact Activities
// panel. Keeping the ordering text server-side prevents every client
// from inventing its own route numbering rules.
label: `${idx + 1}. ${step.label}`,
status: idx < state.progressIndex
? 'complete'
: idx === state.progressIndex
? 'active'
: 'pending',
}))
: [];
return {
id: GAME_ID,
title: 'Scan quest',
status: state.status === 'running' && state.currentQuest ? 'running' : state.status,
headline: state.lastMessage || formatQuestPrompt(state),
detail: orderedList,
progress: {
current: state.progressIndex,
total: state.currentQuest?.steps?.length || 0,
@@ -306,6 +315,16 @@ function getPublicState(rawState, context = {}) {
{ label: 'Skipped', value: state.skippedItems },
{ label: 'Quest points', value: getTopScores(state)[0]?.points || 0 },
],
sections: [
{
title: 'Route',
// This section is the important part of scan quest: players need to
// know the entire ordered route before they drive across the room.
// It is intentionally structured instead of folded into secondary
// text so clients can show progress and the route at the same time.
items: routeItems.length ? routeItems : [{ label: 'add object barcodes to the registry', status: 'pending' }],
},
],
results: state.recentEvents.slice(0, 3).map((event) => ({
label: event.kind === 'timeout' ? 'Skipped' : event.kind,
value: event.label || event.points || '',
@@ -270,12 +270,6 @@ function getPublicState(rawState, context = {}) {
id: GAME_ID,
title: 'Scans per second',
status: state.status,
headline: state.lastMessage,
detail: state.status === 'running'
? `${state.scans.length} scans, ${Math.ceil(remainingMs / 1000)} seconds left`
: state.finalResult
? `${state.finalResult.scanCount} scans in last round`
: 'five minute scan challenge',
scanCount: state.scans.length,
scansPerSecond: Number(currentRate.toFixed(3)),
bestRate: Number(bestRate.toFixed(3)),
@@ -307,6 +301,21 @@ function getPublicState(rawState, context = {}) {
{ label: 'Live scan rate', value: Number(currentRate).toFixed(2) },
{ label: 'World record', value: worldRecordText },
],
sections: [
{
title: 'Round',
// The section carries the richer explanatory text that used to live
// in top-level detail. Keeping it under display means clients have a
// single place to look for all visible game-status information.
items: [
state.status === 'running'
? `${state.scans.length} scans, ${Math.ceil(remainingMs / 1000)} seconds left`
: state.finalResult
? `${state.finalResult.scanCount} scans in last round`
: 'five minute scan challenge',
],
},
],
results: state.recentRounds.slice(0, 3).map((round) => ({
label: 'Round',
value: `${Number(round.scansPerSecond || 0).toFixed(2)} scans per second`,
@@ -56,7 +56,13 @@ function formatList(items = [], limit = 5) {
const visible = values.slice(0, limit);
const extraCount = values.length - visible.length;
const suffix = extraCount > 0 ? `, and ${extraCount} more` : '';
if (extraCount === 1) {
// A single hidden item reads badly as "and 1 more" in chat, especially for
// result summaries where the omitted item is usually just one normal stat.
// Including it is still short and avoids making the bot sound confused.
return values.slice(0, limit + 1).join(', ');
}
const suffix = extraCount > 1 ? `, plus ${extraCount} more` : '';
return `${visible.join(', ')}${suffix}`;
}
@@ -166,7 +172,11 @@ function formatResultSummary(display = {}) {
.filter(Boolean)
: [];
return formatList([primary, ...usefulStats], 3);
// Result summaries are already compact because games normally publish one
// primary line and a few stats. Joining them all avoids vague bot messages
// like "and 1 more" while still leaving the heavier participant lists to the
// bounded formatList helper above.
return [primary, ...usefulStats].filter(Boolean).join('. ');
}
function sendGameStartChat(definition, participants = []) {
@@ -906,6 +916,7 @@ function buildLifecycleGameState(store, { now, selectedDefinition, runningDefini
secondary: store.resultDisplay?.secondary || 'Vote to start another game',
timer: store.resultsUntil ? { label: 'Results clear in', endsAt: store.resultsUntil } : null,
stats: store.resultDisplay?.stats || [],
sections: store.resultDisplay?.sections || [],
results: store.resultDisplay?.results || [],
},
};
@@ -923,6 +934,17 @@ function buildLifecycleGameState(store, { now, selectedDefinition, runningDefini
secondary: 'Get ready',
timer: store.startsAt ? { label: 'Starts in', endsAt: store.startsAt } : null,
stats: [],
sections: [
{
title: 'Players',
// Joining and starting are shared lifecycle states, so they expose
// their own display sections here instead of asking each game to
// explain the same startup flow.
items: participants.length
? participants.map((participant) => getParticipantName(participant)).filter(Boolean)
: ['waiting for counted rovers'],
},
],
results: [],
},
};
@@ -941,6 +963,12 @@ function buildLifecycleGameState(store, { now, selectedDefinition, runningDefini
secondary: `${selectedTitle} needs at least one rover`,
timer: store.joinEndsAt ? { label: 'Join by', endsAt: store.joinEndsAt } : null,
stats: [],
sections: [
{
title: 'How to join',
items: ['scan your rover barcode at the station'],
},
],
results: [],
},
};
@@ -962,6 +990,7 @@ function buildLifecycleGameState(store, { now, selectedDefinition, runningDefini
label: game.title,
value: voteCounts[game.id] || 0,
})),
sections: [],
results: [],
},
};
@@ -978,6 +1007,7 @@ function buildLifecycleGameState(store, { now, selectedDefinition, runningDefini
secondary: 'Vote to start the next round',
timer: null,
stats: [],
sections: [],
results: [],
},
};