mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
better umm.yeah
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -78,8 +78,8 @@
|
||||
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
|
||||
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
|
||||
<title>Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-D_nIHtoI.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CxLsehkZ.css">
|
||||
<script type="module" crossorigin src="/assets/index-ciE1vQRQ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Cxghvfjc.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -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: [],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
5. barcode wiki links
|
||||
6. implement multitabbing prevention using the identity system
|
||||
7. roomba odometer
|
||||
1. in activities tab
|
||||
2. use wheel encoders
|
||||
3. run averages, like 2 meters and how many encoder counts, 20 times
|
||||
4. have global odometers for each rover, tagged based on name
|
||||
8. make google tts the default everywhere but roverd
|
||||
9. fix rover request spam queue cheat
|
||||
10. fix up ALL discord admin commands
|
||||
7. make google tts the default everywhere but roverd
|
||||
8. fix rover request spam queue cheat
|
||||
9. fix up ALL discord admin commands
|
||||
1. make sure all permissions are correct
|
||||
2. fuzzy search all the things
|
||||
3. dont break on multi word nicknames
|
||||
4. make all rs commands work form both the site chat and discord
|
||||
1. make sure all the permissions are correct
|
||||
11. make alert feed.jsx show more alerts at once
|
||||
12. unify typing row and chat row, should be simple
|
||||
13. fix google TTS speeds
|
||||
14. fix this:
|
||||
10. make alert feed.jsx show more alerts at once
|
||||
11. unify typing row and chat row, should be simple
|
||||
12. fix google TTS speeds
|
||||
13. fix this:
|
||||
`Jun 18 15:14:18 roombaserver.local node[216731]: /home/daniel/MultiRoombaRover/server/src/services/roverManager/socketHandlers.js:92
|
||||
Jun 18 15:14:18 roombaserver.local node[216731]: cb({ error: err.message });
|
||||
Jun 18 15:14:18 roombaserver.local node[216731]: ^
|
||||
@@ -36,6 +31,5 @@ Jun 18 15:14:18 roombaserver.local systemd[1]: multirover.service: Consumed 55mi
|
||||
1. get forwarding working with the VPS for in-queue users and spectators
|
||||
2. bandwidth testing
|
||||
3. maybe switch room cams back to real video, with audio?
|
||||
2. overseer LED tesseract
|
||||
3. RF based positional tracking / room map tab
|
||||
4. chromecast monitor youtube search and speakers
|
||||
2. RF based positional tracking / room map tab
|
||||
3. chromecast monitor youtube search and speakers
|
||||
@@ -111,6 +111,66 @@ function StatGrid({ stats, theme }) {
|
||||
);
|
||||
}
|
||||
|
||||
function getSectionItemText(item) {
|
||||
// Game modules may send section items as simple strings or as objects with a
|
||||
// status flag. The panel accepts both so games can stay lightweight until
|
||||
// they need richer per-row state like scan quest's active route item.
|
||||
if (typeof item === 'string') return item;
|
||||
if (!item || typeof item !== 'object') return '';
|
||||
return String(item.label || item.value || item.text || '').trim();
|
||||
}
|
||||
|
||||
function DisplaySections({ sections }) {
|
||||
// display.sections is the shared rich-status contract for barcode games. This
|
||||
// renderer deliberately stays generic: game modules decide the content, while
|
||||
// the Activities panel only applies compact CardFrame-style formatting.
|
||||
const visibleSections = Array.isArray(sections)
|
||||
? sections
|
||||
.map((section) => ({
|
||||
title: String(section?.title || '').trim(),
|
||||
items: Array.isArray(section?.items)
|
||||
? section.items
|
||||
.map((item) => ({
|
||||
text: getSectionItemText(item),
|
||||
status: typeof item === 'object' && item ? item.status : null,
|
||||
}))
|
||||
.filter((item) => item.text)
|
||||
: [],
|
||||
}))
|
||||
.filter((section) => section.title || section.items.length)
|
||||
: [];
|
||||
|
||||
if (!visibleSections.length) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-0.75 grid gap-0.5">
|
||||
{visibleSections.map((section, sectionIndex) => (
|
||||
<div key={`${section.title || 'section'}-${sectionIndex}`} className="surface-muted px-1 py-0.75">
|
||||
{section.title ? (
|
||||
<p className="mb-0.5 text-xs font-semibold text-neutral-300">{section.title}</p>
|
||||
) : null}
|
||||
<div className="grid gap-0.25">
|
||||
{section.items.slice(0, 8).map((item, itemIndex) => (
|
||||
<p
|
||||
key={`${item.text}-${itemIndex}`}
|
||||
className={`break-words text-xs leading-snug ${
|
||||
item.status === 'active'
|
||||
? 'font-semibold text-neutral-50'
|
||||
: item.status === 'complete'
|
||||
? 'text-neutral-500'
|
||||
: 'text-neutral-300'
|
||||
}`}
|
||||
>
|
||||
{item.text}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ParticipantsBlock({ participants, theme }) {
|
||||
const knownParticipants = Array.isArray(participants) ? participants : [];
|
||||
// Participants are server-owned because rover scans, identity lookup, and
|
||||
@@ -305,6 +365,7 @@ export default function BarcodeGamesPanel() {
|
||||
{display.secondary ? (
|
||||
<p className="mt-0.5 break-words text-xs leading-snug text-neutral-300">{display.secondary}</p>
|
||||
) : null}
|
||||
<DisplaySections sections={display.sections} />
|
||||
</div>
|
||||
<div className="surface px-1 py-0.75 text-left lg:text-right">
|
||||
<p className="text-xs font-semibold text-neutral-400">{display.timer?.label || 'Time'}</p>
|
||||
|
||||
@@ -41,6 +41,35 @@ function formatTimer(endsAt, now) {
|
||||
return minutes > 0 ? `${minutes}:${String(seconds).padStart(2, '0')}` : `${seconds}s`;
|
||||
}
|
||||
|
||||
function getSectionItemText(item) {
|
||||
if (typeof item === 'string') return item;
|
||||
if (!item || typeof item !== 'object') return '';
|
||||
return String(item.label || item.value || item.text || '').trim();
|
||||
}
|
||||
|
||||
function normalizeDisplaySections(sections) {
|
||||
if (!Array.isArray(sections)) return [];
|
||||
|
||||
// The scanner screen is intentionally a dumb renderer: games describe their
|
||||
// own rich status through display.sections, and this page only normalizes the
|
||||
// shape enough to avoid crashing on a malformed server payload. That keeps
|
||||
// game-specific logic out of the rover-facing IO page.
|
||||
return sections
|
||||
.map((section) => ({
|
||||
title: String(section?.title || '').trim(),
|
||||
items: Array.isArray(section?.items)
|
||||
? section.items
|
||||
.map((item) => ({
|
||||
text: getSectionItemText(item),
|
||||
status: typeof item === 'object' && item ? item.status : null,
|
||||
}))
|
||||
.filter((item) => item.text)
|
||||
: [],
|
||||
}))
|
||||
.filter((section) => section.title || section.items.length)
|
||||
.slice(0, 3);
|
||||
}
|
||||
|
||||
function playSubmitBeep() {
|
||||
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
||||
if (!AudioContextClass) return;
|
||||
@@ -252,8 +281,9 @@ export default function ScannerContent() {
|
||||
// normal job is still showing the resolved barcode text. Only active lifecycle
|
||||
// states take over the large rover-facing display.
|
||||
const title = showGameDisplay ? display.title || activeGame?.title || 'Barcode games' : lastScan?.label || 'Waiting';
|
||||
const label = showGameDisplay ? display.primary || activeGame?.headline || '' : '';
|
||||
const detail = showGameDisplay ? display.secondary || activeGame?.detail || '' : '';
|
||||
const label = showGameDisplay ? display.primary || '' : '';
|
||||
const secondary = showGameDisplay ? display.secondary || '' : '';
|
||||
const displaySections = showGameDisplay ? normalizeDisplaySections(display.sections) : [];
|
||||
const participants = Array.isArray(barcodeGameState.participants) ? barcodeGameState.participants : [];
|
||||
const syncMessage = !scannerConnectionState.connected
|
||||
? 'scanner offline'
|
||||
@@ -290,11 +320,40 @@ export default function ScannerContent() {
|
||||
<p className="max-w-full break-words text-[6vw] font-bold leading-tight tracking-normal">
|
||||
{label}
|
||||
</p>
|
||||
{detail ? (
|
||||
{secondary ? (
|
||||
<p className="max-w-full break-words text-[4vw] font-bold leading-tight tracking-normal">
|
||||
{detail}
|
||||
{secondary}
|
||||
</p>
|
||||
) : null}
|
||||
{displaySections.length ? (
|
||||
<div className="flex max-w-full flex-col items-center gap-[1vh]">
|
||||
{displaySections.map((section, sectionIndex) => (
|
||||
<div key={`${section.title || 'section'}-${sectionIndex}`} className="max-w-full">
|
||||
{section.title ? (
|
||||
<p className="text-[2.6vw] font-bold leading-tight tracking-normal opacity-80">
|
||||
{section.title}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="flex max-w-full flex-col items-center gap-[0.4vh]">
|
||||
{section.items.slice(0, 8).map((item, itemIndex) => (
|
||||
<p
|
||||
key={`${item.text}-${itemIndex}`}
|
||||
className={`max-w-full break-words text-[3.1vw] leading-tight tracking-normal ${
|
||||
item.status === 'active'
|
||||
? 'font-black'
|
||||
: item.status === 'complete'
|
||||
? 'font-semibold opacity-45'
|
||||
: 'font-bold opacity-85'
|
||||
}`}
|
||||
>
|
||||
{item.text}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{timerText ? (
|
||||
<p className="font-mono text-[7vw] font-black leading-none tracking-normal">
|
||||
{timerText}
|
||||
|
||||
Reference in New Issue
Block a user