feat(events): an event action's progress() on a live run's public page (MODULE_API 1.12.0)
An event action may declare an optional `progress(envelope)`: how its
step stands while the run is live. Core accepts two shapes and
interprets neither:
- a count: { label, left, of }, rendered "Bandits: 3 of 8 left";
- a percentage: { label, percent }, rendered "The Juggernaut: 62%".
How core asks:
- `events/progress.js` asks each step of a live run whose action has
one. It passes the step's own held ledger rows and the reader's
account id, so the module applies its own visibility.
- Each call has a 2 s deadline. A throw, a late answer or any other
shape is a line not shown, never an error on the page.
- One answer per run and reader is cached for 5 s.
- The public event's live occurrence carries the lines as `progress`.
The route takes an optional session for the reader.
- The page renders them under the phase label and re-reads them every
15 s while the event is live.
Core stays game-agnostic: no game's words are in it. Additive, so a
minor bump. Module-uo declares ^1.10.0, registers no action with
progress(), and its actions and pages are unchanged.
From RunicNPC stage 8 (D304, D305).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -14,13 +14,19 @@
|
||||
// branch left a failed load spinning for ever with nothing on screen naming the
|
||||
// problem. Order matters, and the order is error first.
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useParams, useSearchParams, Link } from 'react-router-dom'
|
||||
import PublicLayout from '../../components/PublicLayout.jsx'
|
||||
import PageHeader from '../../components/PageHeader.jsx'
|
||||
import { Loading, ErrorState, EmptyState } from '../../components/PageState.jsx'
|
||||
import { useAsync } from '../../lib/useAsync.js'
|
||||
import { api } from '../../api/client.js'
|
||||
import { eventDateTime, statusWord } from '../../lib/eventCalendar.js'
|
||||
import { eventDateTime, statusWord, progressText } from '../../lib/eventCalendar.js'
|
||||
|
||||
// How often a live event's progress lines are read again while the page is open.
|
||||
// The server caches each run's answer for five seconds, so this is the reader's
|
||||
// cadence, not load on the game.
|
||||
const PROGRESS_REFRESH_MS = 15_000
|
||||
|
||||
export default function EventPage() {
|
||||
const { slug } = useParams()
|
||||
@@ -28,6 +34,27 @@ export default function EventPage() {
|
||||
const run = params.get('run')
|
||||
const { loading, error, data } = useAsync(() => api.publicEvent(slug, run), [slug, run])
|
||||
|
||||
// The live occurrence's progress (MODULE_API 1.12.0), re-read on a timer while
|
||||
// it is live. Only the lines are replaced: re-running the page's own fetch
|
||||
// would blank the page to a spinner every fifteen seconds.
|
||||
const [progress, setProgress] = useState(null)
|
||||
const live = Boolean(data && data.event && data.event.live)
|
||||
useEffect(() => {
|
||||
setProgress(null)
|
||||
if (!live) return undefined
|
||||
let active = true
|
||||
const timer = setInterval(() => {
|
||||
api
|
||||
.publicEvent(slug, run)
|
||||
.then((fresh) => active && setProgress(fresh?.event?.current?.progress || []))
|
||||
.catch(() => {})
|
||||
}, PROGRESS_REFRESH_MS)
|
||||
return () => {
|
||||
active = false
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, [slug, run, live])
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<PublicLayout section="website">
|
||||
@@ -91,6 +118,16 @@ export default function EventPage() {
|
||||
the event is never published. */}
|
||||
{event.current.phase || 'Under way'}
|
||||
</div>
|
||||
{/* How it stands, in the words the steps' modules chose. Core
|
||||
renders the two shapes it accepts and interprets neither. */}
|
||||
{(progress || event.current.progress || []).map((line, i) => {
|
||||
const text = progressText(line)
|
||||
return text ? (
|
||||
<div key={i} className="sans" style={{ marginTop: 4 }}>
|
||||
{text}
|
||||
</div>
|
||||
) : null
|
||||
})}
|
||||
</>
|
||||
) : event.next ? (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user