/** What a run earned. * * The rule this screen exists to enforce: there is no losing screen. A bad run shows * fewer stars and a slower animal, and the primary button still says "Nochmal" with the * focus already on it. Nothing here ever says "failed", nothing is red, and the pearls * always go up - see `pearlsFor` in lib/grading.ts. */ import { useEffect, useRef } from "react"; import { creatureById } from "../lib/aquarium"; import type { CreatureId } from "../lib/aquarium"; import type { RunResult } from "../lib/grading"; import { STAR_THRESHOLDS, visibleAnimals, animalById, animalIndex, animalProgress } from "../lib/grading"; import type { AnimalId } from "../lib/grading"; interface Props { result: RunResult; /** Set when this run opened the next lesson. */ unlockedTitle: string | null; /** Set when this run released a creature into the aquarium. */ newCreature: CreatureId | null; isNewBest: boolean; /** The fastest animal earned on any lesson so far - decides how much of the ladder * may be revealed. */ bestEver: AnimalId | null; onRetry: () => void; onContinue: () => void; /** Null at the end of the curriculum. */ continueLabel: string | null; } export function ResultSheet({ result, unlockedTitle, newCreature, isNewBest, bestEver, onRetry, onContinue, continueLabel, }: Props) { const animal = animalById(result.animal); const ladder = visibleAnimals(result.animal, bestEver); const reached = animalIndex(result.animal); const retryButton = useRef(null); // Enter repeats the run. Fewest keystrokes between "that was fun" and "again". useEffect(() => retryButton.current?.focus(), []); return (
{animal.emoji}
{animal.name}
{[1, 2, 3].map((star) => ( = star ? "none" : "grayscale(1)", opacity: result.stars >= star ? 1 : 0.25, }} > ⭐ ))}
{/* How close the next animal is. A near miss is the strongest reason to press Nochmal, so it is worth showing explicitly. */}
{isNewBest && (
🏅 Neuer Bestwert!
)} {unlockedTitle && (
🔓 Neu: {unlockedTitle}
)} {newCreature && (
{creatureById(newCreature).article} {creatureById(newCreature).name} ist ins Aquarium gezogen!
)} {!result.passed && !unlockedTitle && (
Mit {Math.round(STAR_THRESHOLDS.two * 100)}% Treffern geht es weiter. Fast!
)}
{continueLabel && ( )}
); } /** The animal ladder. * * Everything she has already passed stays in full colour - the run is a climb, and the * rungs below are what shows how far she has come. The rungs above are greyed out but * still legible, because the next animal has to be visible to be worth aiming at. * * Above the dolphin the ladder stops: those animals are not shown at all until they are * reached. A single "?" says the ladder continues without saying what is on it, which * keeps the top end a surprise rather than a distant, discouraging number. */ function AnimalLadder({ animals, reached, moreHidden, }: { animals: readonly { id: string; name: string; emoji: string }[]; reached: number; moreHidden: boolean; }) { return (
{animals.map((entry, i) => { const achieved = i <= reached; const isCurrent = i === reached; return (
{entry.emoji}
); })} {moreHidden && (
)}
); } const buttonStyle: React.CSSProperties = { border: "none", borderRadius: 999, padding: "13px 26px", fontSize: 17, fontWeight: 900, cursor: "pointer", }; function Stat({ label, value }: { label: string; value: string }) { return (
{value}
{label}
); }