Remove the Quallenalarm (jellyfish) typing minigame
Letters-round lessons only had bubbles and jellyfish as eligible arcade modes; with jellyfish gone, bubbles is now always used instead of the two alternating - no lessons need to be dropped. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,7 +27,6 @@ import { Stage } from "./tippen/Stage";
|
||||
import { BubblesRun } from "./tippen/modes/BubblesRun";
|
||||
import { DiveRun } from "./tippen/modes/DiveRun";
|
||||
import { FeedRun } from "./tippen/modes/FeedRun";
|
||||
import { JellyfishRun } from "./tippen/modes/JellyfishRun";
|
||||
import { RaceRun } from "./tippen/modes/RaceRun";
|
||||
import type { CreatureId } from "../lib/tippen/aquarium";
|
||||
import type { Lesson, ModeId } from "../lib/tippen/curriculum";
|
||||
@@ -42,12 +41,12 @@ import { bubbleCountFor } from "../lib/tippen/theme";
|
||||
type Screen = "aquarium" | "map" | "run";
|
||||
|
||||
/** What a mode is handed to draw - see the original App.tsx for why there are only two
|
||||
* shapes for five modes. */
|
||||
* shapes for four modes. */
|
||||
type RunTarget =
|
||||
| { kind: "letters"; letters: readonly string[] }
|
||||
| { kind: "text"; chunks: readonly string[]; text: string; spaceActive: boolean };
|
||||
|
||||
const LETTER_ONLY_MODES: readonly ModeId[] = ["bubbles", "jellyfish"];
|
||||
const LETTER_ONLY_MODES: readonly ModeId[] = ["bubbles"];
|
||||
|
||||
const SHARE_FOR_EMPHASIS: Record<"isolated" | "mixed", number> = { isolated: 0.75, mixed: 0.4 };
|
||||
|
||||
@@ -315,11 +314,7 @@ export function TippenApp({ onExit }: Props) {
|
||||
{screen === "run" && lesson && run && (
|
||||
<>
|
||||
{run.kind === "letters" ? (
|
||||
mode === "jellyfish" ? (
|
||||
<JellyfishRun {...letterProps(run.letters)} />
|
||||
) : (
|
||||
<BubblesRun {...letterProps(run.letters)} />
|
||||
)
|
||||
<BubblesRun {...letterProps(run.letters)} />
|
||||
) : mode === "feed" ? (
|
||||
<FeedRun {...textProps(run)} />
|
||||
) : mode === "race" ? (
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
/** 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<Jellyfish[]>(() => {
|
||||
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 (
|
||||
<div
|
||||
className="view-enter"
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: 16,
|
||||
padding: "0 32px 8px",
|
||||
minHeight: 0,
|
||||
}}
|
||||
>
|
||||
<div style={{ position: "relative", flex: 1, width: "100%", minHeight: 0 }}>
|
||||
{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 (
|
||||
<div
|
||||
key={i}
|
||||
className="tp-jellyfish"
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${spot.left}%`,
|
||||
top: `${spot.top}%`,
|
||||
width: spot.size,
|
||||
height: spot.size,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: spot.size * (isTarget ? 0.4 : 0.3),
|
||||
fontWeight: 900,
|
||||
borderRadius: "50% 50% 42% 42%",
|
||||
color: isTarget ? "oklch(25% 0.05 175)" : "var(--paper)",
|
||||
background: isTarget
|
||||
? "var(--paper)"
|
||||
: `linear-gradient(160deg, oklch(70% 0.13 ${finger?.hue ?? 175} / .38), oklch(45% 0.09 ${finger?.hue ?? 175} / .16))`,
|
||||
border: `2px solid oklch(88% 0.07 ${finger?.hue ?? 175} / ${isTarget ? 0.9 : 0.3})`,
|
||||
boxShadow: isTarget
|
||||
? "0 0 34px oklch(97% 0.01 175 / .55), 0 10px 26px var(--shadow)"
|
||||
: "0 4px 14px var(--shadow)",
|
||||
opacity: isTarget ? 1 : 0.55,
|
||||
transform: isTarget ? "scale(1.12)" : "scale(1)",
|
||||
animation: `dolphinBob ${spot.drift}s ease-in-out infinite`,
|
||||
transition: "opacity 200ms ease, transform 200ms ease, background 200ms ease",
|
||||
}}
|
||||
>
|
||||
{letter === " " ? "␣" : letter}
|
||||
{isTarget && wrong && (
|
||||
<div style={{ position: "absolute", inset: -6, borderRadius: "50%", animation: "wrongShake 260ms ease" }} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Keyboard
|
||||
activeKeys={activeKeys}
|
||||
nextKey={next}
|
||||
progress={progress}
|
||||
mode={progress.settings.keyboardHint}
|
||||
size={36}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user