/** Jellyfish mode - pure key location, nothing else. * * Six jellyfish drift in the water, each showing a letter. One of them glows: that is * the one to zap. There is no line to read and no word to spell, so the only thing * being exercised is "where does this letter live" - which is exactly the skill dive * mode hides behind reading. * * The decoys matter. Showing only the target turns this into the bubble mode; showing * five wrong letters next to it means she has to find *her* letter before she can type * it, which is the searching step that eventually goes away. */ import { useMemo } from "react"; import { currentChar } from "../../../lib/tippen/engine"; import { fingerOf } from "../../../lib/tippen/fingers"; import { mulberry32 } from "../../../lib/tippen/generator"; import type { RunResult } from "../../../lib/tippen/grading"; import type { Progress } from "../../../lib/tippen/progress"; import { useRun } from "../../../hooks/useTippenRun"; import { Keyboard } from "../Keyboard"; interface Props { letters: readonly string[]; activeKeys: readonly string[]; progress: Progress; paused: boolean; onFinished: (result: RunResult) => void; } /** How many jellyfish are in the water at once, target included. */ const JELLYFISH_COUNT = 6; interface Jellyfish { left: number; top: number; drift: number; size: number; } export function JellyfishRun({ letters, activeKeys, progress, paused, onFinished }: Props) { const text = letters.join(""); const { state, wrong } = useRun({ target: text, sound: progress.settings.sound, paused, onFinished, }); const next = currentChar(state); // Fixed positions, seeded once: jellyfish that jump to a new spot on every keystroke // would make the searching step impossible rather than merely hard. const spots = useMemo(() => { const rng = mulberry32(text.length * 31 + 7); return Array.from({ length: JELLYFISH_COUNT }, () => ({ left: 10 + rng() * 76, top: 6 + rng() * 66, drift: 3 + rng() * 3, size: 74 + rng() * 26, })); }, [text]); /** The decoys shown alongside the target: other active keys, never the target itself, * and stable for as long as the target is. */ const decoys = useMemo(() => { if (!next) return []; const rng = mulberry32(state.index * 101 + 13); const others = activeKeys.filter((key) => key !== next && key !== " "); const shuffled = [...others].sort(() => rng() - 0.5); return shuffled.slice(0, JELLYFISH_COUNT - 1); }, [next, state.index, activeKeys]); // Which jellyfish carries the target. Moves around so it is not always the same one. const targetSlot = next ? state.index % JELLYFISH_COUNT : -1; return (
{spots.map((spot, i) => { const isTarget = i === targetSlot; const letter = isTarget ? next : decoys[i > targetSlot ? i - 1 : i]; if (!letter) return null; const finger = fingerOf(letter); return (
{letter === " " ? "␣" : letter} {isTarget && wrong && (
)}
); })}
); }