Spaces:
Sleeping
Sleeping
| // 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; | |
| } | |