import { useEffect, useRef, useState, type CSSProperties, type ReactNode, } from "react"; import { LAPS } from "./steerConfig"; import { polar } from "./physics"; // First-launch tutorial: a tiny 3-slide carousel shown once (after the player // hits "Larguez les amarres") over the frozen sea. Skipping or finishing marks // it seen so it never shows again. Illustrations reuse the game's real UI // components (helm pads + tiller, crew-call buttons, HUD metrics) so what the // player sees here is exactly what they'll drive. // Slide 1 - the helm: the two steering cards with the wooden tiller nestled in // their scoop, rebuilt from the live .ctrl markup (positioning overridden by // .tt-helm). function HelmArt() { return (
); } // Slide 2 - points of sail: a compact replica of the game's easy-mode "allure // ring" (same angles, labels and dashed grey style as steerRender.allureRing). // Wind blows from the top; 0deg is head-to-wind (no-go), 180deg is dead run. const ALLURES: [string, number][] = [ ["Près", 52], ["Bon plein", 69], ["Travers", 89], ["Largue", 115], ["Grand largue", 140], ]; const ALLURE_DIVS = [43, 60, 78, 100, 130, 150]; // Wedge half-angles, straight from physics.ts (NOGO / GYBE). const NOGO = 43; const GYBE = 30; function AllureArt() { const cx = 140; const cy = 104; const R = 82; const rDiv = 30; // inner radius where sector dividers start const rLab = R + 9; // labels: their INNER edge sits on this circle (even spacing) const pt = (aDeg: number, r: number, s = 1) => { const a = (aDeg * Math.PI) / 180; return [cx + s * r * Math.sin(a), cy - r * Math.cos(a)] as const; }; // Pie slice symmetric about the vertical axis: `edge` is the off-wind angle of // each side. Upwind (edge=NOGO) sweeps through the top; downwind // (edge=180-GYBE) sweeps through the bottom. const wedgeTop = (edge: number) => { const l = pt(edge, R, -1); const r = pt(edge, R, 1); return `M ${cx} ${cy} L ${l[0]} ${l[1]} A ${R} ${R} 0 0 1 ${r[0]} ${r[1]} Z`; }; const wedgeBottom = (edge: number) => { const l = pt(edge, R, -1); const r = pt(edge, R, 1); return `M ${cx} ${cy} L ${r[0]} ${r[1]} A ${R} ${R} 0 0 1 ${l[0]} ${l[1]} Z`; }; // Relative-speed polar: the boat's actual target speed at each allure, straight // from physics.polar(). Radius at angle a = polar(a) * rSpeed, mirrored on both // tacks -> a closed blob that's fat where the boat is fast (reach), pinched to // nothing in the no-go, and only medium dead downwind. const rSpeed = 66; // max speed radius, kept inside the ring (R = 82) const speedD = (() => { const step = 4; const seg: string[] = []; for (let a = 0; a <= 180; a += step) { const [x, y] = pt(a, polar(a) * rSpeed, 1); seg.push(`${x.toFixed(1)} ${y.toFixed(1)}`); } for (let a = 180; a >= 0; a -= step) { const [x, y] = pt(a, polar(a) * rSpeed, -1); seg.push(`${x.toFixed(1)} ${y.toFixed(1)}`); } return "M " + seg.join(" L ") + " Z"; })(); return ( ); } // Slide 3 - the crew calls: the two "Paré à ..." buttons, straight from the live // .annbtn markup. They "self-click" in turn (see .tt-calls animation) to mime the // announce ritual; the danger of skipping it lives in the slide text. function CallsArt() { return ( ); } // Slide 4 - the bearing: a static replica of the game's off-screen mark pointer // (steerRender.drawOffscreenArrow) - a red arrow aimed at the next buoy, its // distance readout, and the small curved glyph telling you which side to round. function BearingArt() { return ( ); } // Slide 5 - the objective: the HUD metric trio the player chases (laps, buoys, // time), reusing the live .metric / .eyebrow / .speed styles. const GOAL_BUOYS = 2; // buoys per lap on the demo windward-leeward const GOAL_SECS_PER_BUOY = 4; // demo pace: a buoy is rounded every few seconds const GOAL_LOOP = LAPS * GOAL_BUOYS * GOAL_SECS_PER_BUOY; // full "race" before it replays function GoalArt() { // A tiny looping "race": the clock ticks up every second while the buoy and // lap counters climb like a real run (buoy 1/2 -> 2/2, then next lap), then it // resets so the counters are always visibly moving. const [t, setT] = useState(0); useEffect(() => { const id = window.setInterval(() => setT((s) => (s + 1) % GOAL_LOOP), 1000); return () => window.clearInterval(id); }, []); const buoyIndex = Math.floor(t / GOAL_SECS_PER_BUOY); // 0..(LAPS*GOAL_BUOYS-1) const lap = Math.floor(buoyIndex / GOAL_BUOYS) + 1; const buoy = (buoyIndex % GOAL_BUOYS) + 1; const time = `${Math.floor(t / 60)}:${(t % 60).toString().padStart(2, "0")}`; // Bounce the Tour / Bouées counters on each increment, exactly like the live // HUD: retrigger the game's `pop` animation (remove class -> reflow -> add). // Seed the "previous" refs with the initial values so the counters DON'T pop on // arrival - only on the first real change and onward. const lapRef = useRef