Add a persistent high score table
Retiring writes each player's closing balance to a top-ten table in localStorage, reachable from the title screen and the standings, with the summer just finished picked out in yellow. Stored rows are user-editable, so every field is validated on load and malformed entries are dropped. Storage being unavailable is a normal case, not an error: the game plays on with an empty table. Also stops the status line claiming DAY 00 on screens that have no day.
This commit is contained in:
@@ -11,11 +11,13 @@ export function GameOverScreen({
|
||||
history,
|
||||
days,
|
||||
onRestart,
|
||||
onScores,
|
||||
}: {
|
||||
players: Player[]
|
||||
history: DayResult[]
|
||||
days: number
|
||||
onRestart: () => void
|
||||
onScores: () => void
|
||||
}) {
|
||||
const ranked = [...players].sort((a, b) => b.assets - a.assets)
|
||||
const best = history.reduce<DayResult | null>(
|
||||
@@ -69,6 +71,7 @@ export function GameOverScreen({
|
||||
<Btn kind="primary" onClick={onRestart}>
|
||||
PLAY AGAIN
|
||||
</Btn>
|
||||
<Btn onClick={onScores}>HIGH SCORES</Btn>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useState } from 'react'
|
||||
import { dollars } from '../game/engine'
|
||||
import { MAX_SCORES, type Score } from '../game/highscores'
|
||||
import { Btn, Line } from './Crt'
|
||||
|
||||
const shortDate = (at: number) =>
|
||||
new Date(at).toLocaleDateString(undefined, { day: '2-digit', month: 'short' }).toUpperCase()
|
||||
|
||||
export function HighScoreScreen({
|
||||
scores,
|
||||
highlight,
|
||||
onBack,
|
||||
onClear,
|
||||
onBlip,
|
||||
}: {
|
||||
scores: Score[]
|
||||
highlight: string[]
|
||||
onBack: () => void
|
||||
onClear: () => void
|
||||
onBlip: () => void
|
||||
}) {
|
||||
const [confirming, setConfirming] = useState(false)
|
||||
const fresh = new Set(highlight)
|
||||
|
||||
return (
|
||||
<div className="stack">
|
||||
<Line className="center inv-line">$$ BEST STANDS IN LEMONSVILLE $$</Line>
|
||||
<Line />
|
||||
|
||||
{scores.length === 0 ? (
|
||||
<>
|
||||
<Line className="center dim">NO STANDS HAVE CLOSED THEIR BOOKS YET.</Line>
|
||||
<Line />
|
||||
<Line className="center dim">RETIRE AT THE END OF A SUMMER TO</Line>
|
||||
<Line className="center dim">TAKE A PLACE ON THIS LIST.</Line>
|
||||
</>
|
||||
) : (
|
||||
scores.map((s, i) => (
|
||||
<div className={`report-row score-row ${fresh.has(s.id) ? 'is-new' : ''}`} key={s.id}>
|
||||
<span className="report-label">
|
||||
{String(i + 1).padStart(2, ' ')}. {s.name}
|
||||
{s.broke ? ' (BROKE)' : ''}
|
||||
</span>
|
||||
<span className="report-dots" aria-hidden />
|
||||
<span className="score-days dim">
|
||||
{s.days}D · {shortDate(s.at)}
|
||||
</span>
|
||||
<span className="report-value money">{dollars(s.assets)}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
<Line />
|
||||
<Line className="center dim">TOP {MAX_SCORES}, KEPT IN THIS BROWSER.</Line>
|
||||
<Line />
|
||||
<div className="row center">
|
||||
<Btn kind="primary" onClick={onBack}>
|
||||
BACK
|
||||
</Btn>
|
||||
{scores.length > 0 && (
|
||||
<Btn
|
||||
onClick={() => {
|
||||
onBlip()
|
||||
if (confirming) {
|
||||
onClear()
|
||||
setConfirming(false)
|
||||
} else {
|
||||
setConfirming(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{confirming ? 'REALLY WIPE?' : 'WIPE TABLE'}
|
||||
</Btn>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,10 +6,12 @@ const BANNER = bannerRows('LEMONADE')
|
||||
export function TitleScreen({
|
||||
onStart,
|
||||
onInstructions,
|
||||
onScores,
|
||||
seed,
|
||||
}: {
|
||||
onStart: () => void
|
||||
onInstructions: () => void
|
||||
onScores: () => void
|
||||
seed: number
|
||||
}) {
|
||||
return (
|
||||
@@ -29,6 +31,7 @@ export function TitleScreen({
|
||||
START
|
||||
</Btn>
|
||||
<Btn onClick={onInstructions}>INSTRUCTIONS</Btn>
|
||||
<Btn onClick={onScores}>HIGH SCORES</Btn>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user