// Static configuration for the "learn to steer" game: palette, tuning knobs, // rig geometry, telltale model and the pure telltale-strand builder. Everything // here is stateless (no game/closure state), so it lives outside the component. import { NOGO } from "./physics"; export const COLORS = { water: "#d9e9ec", waterDeep: "#a8ccd6", crest: "#f2fafb", ink: "#163642", inkSoft: "#3f5f6b", port: "#c4452f", stbd: "#2e7d5b", luff: "#dd8a26", hull: "#f6f1e7", // Rig accent: a Glénans-style red for the sails (and the rudder, which shares // the same tone). A hair deeper than the port-buoy red so the two don't blur. sail: "#b8382a", }; export const MAXSPD = 6.6; // kn at best polar (J/80 broad-reach peak, ~8 kn TWS) // Sail-trim efficiency: luffing / over-sheeted sails lose drive, so a combined // luff factor (main-dominant, plus a smaller headsail share) scales the speed // target down (0 = full and drawing, 1 = flogging). This is what makes "paré à // empanner" without gybing - the main hauled amidships on a run - actually cost // speed, and what makes pinching upwind bite. 0 = disabled, 1 = a full flog // kills all drive. On a settled beat/reach the sails sit at ~0 luff, so a // well-trimmed boat pays no penalty. export const LUFF_DRIVE_PENALTY = 0.8; export const TWS = 8; // true wind speed, same arbitrary units as boat speed export const BOAT = 2.1; // visual scale of the boat (hull + rig) export const WAKE_STEP = 7; // distance (world px) between recorded wake points export const WAKE_MAX = 90; // max wake points -> overall wake length export const WAKE_LIFE_MS = 2200; // how long a wake point lives, so it fades when slow // Mark rounding uses the real racing rule (RRS 28 "string rule") with NO maximum // radius - round as wide or tight as you like. The geometry lives in // steerPhysics.roundingProgress. // Touching a mark is a foul (RRS 31). We don't force a penalty turn; instead the // hit kills most of the boat's way and jolts the screen - the lost speed IS the // penalty. BUOY_HIT_R is how close (world px) the boat centre must come to a buoy // to count as a touch; BUOY_HIT_KEEP is the fraction of speed left after a hit. export const BUOY_HIT_R = 24; export const BUOY_HIT_KEEP = 0.3; export const POP_MS = 560; // duration of the "validated" pop animation on a rounded buoy export const POP_SCALE = 0.85; // extra scale at the peak of the pop (1 + this) export const LAPS = 2; // windward-leeward loops before the finish (2-4 is a normal race) // Rig geometry, in unscaled local units (x = starboard, y = aft, -y = bow). export const MAST_Y = -2.3; // mainsail tack / boom pivot (rig stretched with the hull) export const BOOM_LEN = 16.2; // mainsail foot length (top-down, trimmed a touch shorter) export const JIB_TACK_Y = -23; // headsail tack at the forestay (rig stretched with hull) export const JIB_LEN = 12.5; // headsail foot length (top-down, trimmed a touch shorter) export const RIG_AOA = 16; // target angle of attack held by the sheets (deg) // Headsail blanketing deep downwind: past ~150deg off the wind the mainsail // steals the jib's air, so instead of just fluttering the headsail DEVENTS - // it goes limp and its clew collapses inboard toward the forestay. `START`.. // `FULL` is the off-wind range (deg) over which it builds, and `RATE` how fast // it sets in over time (per s) so the collapse happens "after a moment", not // instantly. It clears quickly the instant you head back up. export const JIB_BLANKET_START = 150; // off-wind angle where the main starts to shadow the jib export const JIB_BLANKET_FULL = 173; // by here (near dead run) the jib is fully starved export const JIB_BLANKET_RATE = 0.9; // build-in speed (per s); low = sets in gradually // "Paré à empanner": the mainsheet is hauled in toward the centreline as the // crew gets ready, so the boom crosses under control instead of slamming across // from right out. This is the boom angle (deg off dead-aft) it's trimmed to. export const GYBE_CENTER_DEG = 12; // Telltale (penons) driven by the REAL angle of attack of the jib, read against // a sheet trim that LAGS the helm (the trim is never instant). Both strands react // to steering, and settle back once the sheet catches up: // - head up faster than the sheet trims in -> AoA drops -> WINDWARD lifts; // - bear away faster than it eases -> AoA climbs -> LEEWARD stalls (the sheet is // momentarily over-trimmed for the new, lower course); // - settled on any beat -> AoA ~ RIG_AOA -> both stream; // - near the no-go (sheet on its stop) the windward stays lifted; deep downwind // (push mode) both go slack. export const TT_TRIM_RATE = 0.8; // how fast the trim chases the helm (per s); lower = laggier // Both strands break on the SAME lag gap (jibTarget - trimLagDeg, in sheet deg), // so they're equally sensitive: negative gap (headed up) lifts the windward, // positive gap (bore away) stalls the leeward. export const TT_LAG_DEAD = 1.5; // gap tolerated before a strand starts to break (deg) export const TT_LAG_SPAN = 9; // extra gap over which it fully breaks (deg) // The windward strand also breaks when you point ABOVE the beat you're working: // past this off-wind limit (heading up toward the no-go) it lifts, regardless of // trim - it's the "you've overstood the close-hauled" cue. export const TT_PINCH_LIMIT = NOGO + 6; // ~49deg: the working close-hauled limit // Deep downwind the sail is in push mode: the flow separates and both go slack. export const TT_RUN_START = 94; // right at the beam reach: the flow lets go here export const TT_RUN_FULL = 104; // by the largue both strands are fully slack (sharp drop) // Helm & steering feel - every knob is here so it can be tuned in one place. // The helm and the boat are COUPLED through the tiller: the boat's heading comes // only from the rudder angle (`tiller`), and the rig's self-steering moment // (`sailHelm`) loads the free tiller toward a rest position, so the barre (both // the rudder on the boat and the widget below) visibly reacts to the force. A // held pad drives the tiller across and it stays where it's left; let go and the // flow torque takes over. `sailHelm` is signed and driven by rig pressure: // WEATHER helm (rounds up toward the wind) at close-hauled/reach, fading then // REVERSING into LEE helm (bears away, the other way) deep downwind. export const HELM = { rate: 2.7, // how fast a held pad/key drives the tiller across (per s) load: 2.2, // how fast the flow torque loads the free tiller toward its rest rudderTurn: 78, // heading change at full helm (deg/s), scaled by speed rudderFloor: 0.4, // rudder authority with no way on (0..1 of full) weather: 9, // max weather helm (deg/s): round-up toward the wind, peaks abeam lee: 5, // max lee helm (deg/s): bear-away the OTHER way deep downwind leeStart: 135, // off-wind angle (deg) where lee helm starts to build in powerCap: 1.4, // clamp on the rig-pressure proxy (apparent > TWS upwind) // Hull directional stability: the keel/hull resist yaw, so the boat TRACKS // (disturbances decay) instead of spinning off any imbalance. At speed the // steady turn rate for full helm is ~ rudderTurn / yawDamp. yawDamp: 1.9, // directional stability (steady-turn divisor, grows with speed) yawInertia: 3.5, // yaw response rate (per s): higher = snappier, less lag }; export interface Boat { x: number; y: number; hdg: number; spd: number; } // Build a telltale as a thin ribbon of cloth: a centreline that leaves the luff // at `angleDeg` (0 = streaming aft, <0 lifts up, >0 droops down) and ripples // along its length. The free tip flaps most (anchored end stays put), so a // broken/limp strand waves like real fabric instead of a rigid rotated curve. export function buildStrand( angleDeg: number, amp: number, phase: number, len = 30, ) { const ang = (angleDeg * Math.PI) / 180; const dx = Math.cos(ang); const dy = Math.sin(ang); const px = -Math.sin(ang); // unit perpendicular, for the ripple offset const py = Math.cos(ang); let d = "M0 0"; const N = 7; for (let i = 1; i <= N; i++) { const t = i / N; const dist = t * len; const rip = amp * Math.sin(t * 4.4 + phase) * t; const x = dx * dist + px * rip; const y = dy * dist + py * rip; d += ` L${x.toFixed(1)} ${y.toFixed(1)}`; } return d; }