/* Ride: drop into a cockpit-follow camera on a helicopter circuit or an airliner, with a minimal HUD and a social reaction layer (fictional handles) — the communication aspect of watching together. */ const RIDE = (GE.ride = { on: false }); const CHAT_HELI = [ ['rotorwash_ret', 'I flew that exact circuit 40 times. Downwind is where your instructor goes quiet.'], ['kora_maps', 'Watching from Lisbon. The stagefields look like a chessboard from here'], ['skywatch_ana', 'Look at the spacing — four aircraft in the pattern and nobody bunching up'], ['rotorwash_ret', 'Turning base now. Watch the airspeed bleed off'], ['hydro_nerd', 'is this live?? the shadows match my window right now'], ['geodesy_dan', 'MLAT puts them at ±15 m here. Cool to see it smooth'], ['ais_owl', 'Came from the Suez queue thread. This is so much calmer'], ['rotorwash_ret', 'Short final. Hover check. Then do it again. And again.'], ]; const CHAT_JET = [ ['skywatch_ana', 'Cruise is the best seat in the house. Look at the terminator out the left side'], ['kora_maps', 'You can see the cloud deck the forecast promised'], ['geodesy_dan', 'Ground speed is up — jet stream tailwind'], ['ais_owl', 'There’s a whole convoy of ships under us right now'], ]; RIDE.start = function (p) { const W = GE.world; if (APP.mode === 'situation') { GE.sheet.close(true); } RIDE.p = p; RIDE.on = true; RIDE.yaw = 0; RIDE.pitch = 0; RIDE.zoom = 1; if (APP.tab !== 'globe') { APP.tab = 'globe'; $('#app').dataset.tab = 'globe'; $$('.pane .view').forEach((v) => v.classList.remove('on')); } APP.setMode('ride'); W.layerById.helis.on = true; const isHeli = p.kind === 'heli' || p.kind === 'situation'; const hc = isHeli ? GE.HELI_CIRCUITS[p.id || 0] : null; const fl = !isHeli ? GE.FLIGHTS[p.id] : null; RIDE.hc = hc; RIDE.fl = fl; const state = () => { const t = W.t; if (hc) { const a = GE.heliAt(hc, t), b = GE.heliAt(hc, t + 1.5); const hd = geo.bearing(a.lat, a.lon, b.lat, b.lon); const c = GE.heliAt(hc, t - 1.5); const hd0 = geo.bearing(c.lat, c.lon, a.lat, a.lon); let turn = hd - hd0; if (turn > Math.PI) turn -= TAU; if (turn < -Math.PI) turn += TAU; return { lat: a.lat, lon: a.lon, alt: a.altKm, hdg: hd, roll: clamp(turn * 6, -0.45, 0.45), spd: a.spd, vs: (b.altKm - c.altKm) / 3 * 3281 * 60 }; } const a = GE.flightAt(fl, t) || GE.flightAt(fl, t + 600); const b = GE.flightAt(fl, t + 4) || a; return { lat: a.lat, lon: a.lon, alt: a.altKm, hdg: geo.bearing(a.lat, a.lon, b.lat, b.lon), roll: 0, spd: fl.speed / 1.852, vs: (b.altKm - a.altKm) / 4 * 3281 * 60 }; }; RIDE.state = state; const rig = { blendTime: 1.8, pose() { const s = state(); RIDE.s = s; const pitch = (hc ? -0.16 : -0.05) + RIDE.pitch; return Cam.pose(s.lat, s.lon, s.alt + (hc ? 0.003 : 0.002), s.hdg + RIDE.yaw, pitch, s.roll * 0.6, (hc ? 64 : 58) * D2R / RIDE.zoom); }, look(dx, dy) { RIDE.yaw -= dx * 0.004; RIDE.pitch = clamp(RIDE.pitch - dy * 0.003, -0.9, 0.5); }, zoom(r) { RIDE.zoom = clamp(RIDE.zoom / r, 0.6, 3); }, }; Cam.setRig(rig); RIDE.renderUI(isHeli); RIDE.chatI = 0; clearInterval(RIDE._ci); RIDE._ci = setInterval(() => RIDE.nextChat(), 2600); setTimeout(() => RIDE.nextChat(), 900); clearInterval(RIDE._ri); RIDE._ri = setInterval(() => { if (Math.random() < 0.7) RIDE.heart(); }, 700); haptic([10, 40, 10]); }; RIDE.renderUI = function (isHeli) { const el = $('#ride'); const nm = isHeli ? RIDE.hc.cs : RIDE.fl.cs; const tp = isHeli ? `${RIDE.hc.type} · ${RIDE.hc.field || 'orbit'} circuit` : `${RIDE.fl.type} · ${GE.flightAt(RIDE.fl, GE.world.t)?.from.iata} → ${GE.flightAt(RIDE.fl, GE.world.t)?.to.iata}`; el.innerHTML = `