mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 07:12:21 +00:00
An application subject's camera step is now capped at its own update interval plus 0.1 s rather than at 0.1 s, so at 500 ms its heading slew, ground lift and anchor correction run at their configured rates; a long stall still moves it no further than one late update. The built-in views keep their 0.1 s cap. When groundClamp turns false the lift the floor gave goes at the rate it came, not at once. A HUD view with no speed has no speed readout, and a progress line with no summary is hidden from assistive technology instead of being an unnamed image.
490 lines
18 KiB
JavaScript
490 lines
18 KiB
JavaScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import {
|
||
altitudeRulerStep,
|
||
altitudeRulerTicks,
|
||
altitudeRulerCurveInset,
|
||
bearingBetweenCoordinates,
|
||
cockpitAnchorCorrectionStep,
|
||
cockpitAltitudeDisplayFt,
|
||
cockpitGroundSafeHeight,
|
||
cockpitSubjectStepSec,
|
||
cockpitSurfaceWaitExpired,
|
||
cockpitUiUpdateDue,
|
||
COCKPIT_SUBJECT_LIFT_MPS,
|
||
easeCockpitLift,
|
||
compassDivisions,
|
||
compassMarkerSlot,
|
||
scrollingCompassDivisions,
|
||
formatAltitudeRulerTick,
|
||
formatCockpitContextScope,
|
||
formatCockpitEmergency,
|
||
formatCompassDivision,
|
||
formatHeadingReadout,
|
||
formatSpeedRulerTick,
|
||
normalizeHeading,
|
||
relativeBearing,
|
||
resolveCockpitContextReadout,
|
||
resolveHudRailLayout,
|
||
resolveTrackedAircraftInfo,
|
||
slewHeading,
|
||
speedRulerStep,
|
||
speedRulerTicks,
|
||
} from './cockpitMath.js';
|
||
|
||
test('cockpit presentation updates are throttled independently of camera frames', () => {
|
||
assert.equal(cockpitUiUpdateDue(1000, 0, 100), true);
|
||
assert.equal(cockpitUiUpdateDue(1099, 1000, 100), false);
|
||
assert.equal(cockpitUiUpdateDue(1100, 1000, 100), true);
|
||
assert.equal(cockpitUiUpdateDue(10, 1000, 100), true);
|
||
});
|
||
|
||
test('grounded cockpit surface acquisition has a bounded wait', () => {
|
||
assert.equal(cockpitSurfaceWaitExpired(5999, 1000, 5000), false);
|
||
assert.equal(cockpitSurfaceWaitExpired(6000, 1000, 5000), true);
|
||
});
|
||
|
||
test('cockpit anchor correction cannot turn a forward step into a reversal', () => {
|
||
const speedMps = 200;
|
||
const dtSec = 0.1;
|
||
const forwardStepM = speedMps * dtSec;
|
||
const backwardCorrectionM = cockpitAnchorCorrectionStep(1000, speedMps, dtSec);
|
||
assert.ok(backwardCorrectionM > 0);
|
||
assert.ok(backwardCorrectionM < forwardStepM);
|
||
});
|
||
|
||
test('cockpit anchor correction bounds late-fix catch-up and render stalls', () => {
|
||
assert.ok(cockpitAnchorCorrectionStep(5000, 250, 2) <= 5.5);
|
||
assert.equal(cockpitAnchorCorrectionStep(0, 250, 0.1), 0);
|
||
assert.equal(cockpitAnchorCorrectionStep(Number.NaN, 250, 0.1), 0);
|
||
});
|
||
|
||
test('an application subject on a slow cadence is corrected for its whole step', () => {
|
||
// The built-in views' step stays capped at 0.1 s...
|
||
const capped = cockpitAnchorCorrectionStep(100, 0, 0.5);
|
||
assert.equal(capped, cockpitAnchorCorrectionStep(100, 0, 0.1));
|
||
// ...while a subject updated every 500 ms is corrected for all of it.
|
||
const whole = cockpitAnchorCorrectionStep(100, 0, 0.5, 0.5);
|
||
assert.ok(Math.abs(whole / capped - 5) < 1e-9);
|
||
assert.equal(cockpitAnchorCorrectionStep(100, 0, 0.5, Number.NaN), capped);
|
||
});
|
||
|
||
test('an application subject steps by its interval, never by a long stall', () => {
|
||
assert.equal(cockpitSubjectStepSec(16, 0), 0.016);
|
||
assert.equal(cockpitSubjectStepSec(500, 500), 0.5);
|
||
assert.equal(cockpitSubjectStepSec(520, 500), 0.52);
|
||
// A hidden tab's minutes count as one late update.
|
||
assert.equal(cockpitSubjectStepSec(60_000, 500), 0.6);
|
||
assert.equal(cockpitSubjectStepSec(60_000, 0), 0.1);
|
||
assert.equal(cockpitSubjectStepSec(-5, 500), 0);
|
||
assert.equal(cockpitSubjectStepSec(Number.NaN, 500), 0);
|
||
assert.equal(cockpitSubjectStepSec(1000, Number.NaN), 0.1);
|
||
});
|
||
|
||
test('cockpit camera clearance never falls below the rendered ground floor', () => {
|
||
assert.equal(cockpitGroundSafeHeight(47, 57, 12), 69);
|
||
assert.equal(cockpitGroundSafeHeight(1200, 57, 12), 1200);
|
||
assert.equal(cockpitGroundSafeHeight(47, null, 12), 47);
|
||
assert.equal(cockpitGroundSafeHeight(47, 57, -4), 57);
|
||
});
|
||
|
||
test('cockpit altitude reads zero on the ground without changing airborne MSL', () => {
|
||
assert.equal(cockpitAltitudeDisplayFt(200.56, true), 0);
|
||
assert.ok(Math.abs(cockpitAltitudeDisplayFt(3048, false) - 10000) < 0.01);
|
||
assert.equal(cockpitAltitudeDisplayFt(null, false), null);
|
||
});
|
||
|
||
test('cockpit Context scope distinguishes radius-complete feeds from viewport installations', () => {
|
||
assert.equal(
|
||
formatCockpitContextScope('TEST123', 250000, 'CURRENT VIEWPORT ONLY'),
|
||
'TEST123 · 250 KM AIR/SEA WINDOW · INSTALLATIONS CURRENT VIEWPORT ONLY',
|
||
);
|
||
assert.equal(
|
||
formatCockpitContextScope('TEST123', 250000),
|
||
'TEST123 · 250 KM AIR/SEA WINDOW',
|
||
);
|
||
});
|
||
|
||
test('cockpit Context scope preserves zero and replaces missing values intentionally', () => {
|
||
assert.equal(
|
||
formatCockpitContextScope('', 0),
|
||
'— · 0 KM AIR/SEA WINDOW',
|
||
);
|
||
assert.equal(
|
||
formatCockpitContextScope(undefined, Number.NaN, {}),
|
||
'— · — KM AIR/SEA WINDOW',
|
||
);
|
||
});
|
||
|
||
test('altitude ruler chooses tighter intervals close to the surface', () => {
|
||
assert.equal(altitudeRulerStep(1200), 100);
|
||
assert.equal(altitudeRulerStep(9000), 250);
|
||
assert.equal(altitudeRulerStep(34000), 500);
|
||
assert.equal(altitudeRulerStep(Number.NaN), 500);
|
||
});
|
||
|
||
test('altitude ruler ticks move fractionally behind the fixed pointer', () => {
|
||
const ticks = altitudeRulerTicks(34875);
|
||
assert.equal(ticks.length, 9);
|
||
assert.equal(ticks[4].valueFt, 34500);
|
||
assert.equal(ticks[4].slot, -0.75);
|
||
assert.equal(ticks[5].valueFt, 35000);
|
||
assert.equal(ticks[5].slot, 0.25);
|
||
assert.equal(ticks[5].major, true);
|
||
});
|
||
|
||
test('altitude ruler clamps below ground and formats fixed-width labels', () => {
|
||
assert.equal(altitudeRulerTicks(-20)[4].valueFt, 0);
|
||
assert.equal(formatAltitudeRulerTick(900), '00900');
|
||
assert.equal(formatAltitudeRulerTick(34874.6), '34875');
|
||
assert.equal(formatAltitudeRulerTick(null), '-----');
|
||
});
|
||
|
||
test('altitude ruler curve follows the circular keyhole at wide aspect ratios', () => {
|
||
assert.equal(altitudeRulerCurveInset(0), 0);
|
||
assert.equal(altitudeRulerCurveInset(-4), altitudeRulerCurveInset(4));
|
||
assert.ok(Math.abs(altitudeRulerCurveInset(4) - 0.231625) < 0.00001);
|
||
});
|
||
|
||
test('speed ruler chooses useful taxi, regional, and cruise intervals', () => {
|
||
assert.equal(speedRulerStep(45), 10);
|
||
assert.equal(speedRulerStep(180), 20);
|
||
assert.equal(speedRulerStep(460), 25);
|
||
assert.equal(speedRulerStep(Number.NaN), 25);
|
||
});
|
||
|
||
test('speed ruler ticks move smoothly behind a fixed pointer', () => {
|
||
const ticks = speedRulerTicks(467);
|
||
assert.equal(ticks.length, 9);
|
||
assert.equal(ticks[4].valueKt, 450);
|
||
assert.ok(Math.abs(ticks[4].slot + 0.68) < 0.00001);
|
||
assert.equal(ticks[5].valueKt, 475);
|
||
assert.ok(Math.abs(ticks[5].slot - 0.32) < 0.00001);
|
||
assert.equal(formatSpeedRulerTick(8), '008');
|
||
assert.equal(formatSpeedRulerTick(null), '---');
|
||
});
|
||
|
||
test('normalizeHeading wraps negative and oversized courses', () => {
|
||
assert.equal(normalizeHeading(-1), 359);
|
||
assert.equal(normalizeHeading(721), 1);
|
||
});
|
||
|
||
test('slewHeading follows the shortest arc across north', () => {
|
||
assert.equal(slewHeading(350, 10, 5), 355);
|
||
assert.equal(slewHeading(10, 350, 5), 5);
|
||
});
|
||
|
||
test('compass divisions remain ordered across north', () => {
|
||
assert.deepEqual(compassDivisions(359), [270, 300, 330, 0, 30, 60, 90]);
|
||
});
|
||
|
||
test('compass marks sit in division slots off the caret, or off the tape', () => {
|
||
// Measured from the exact heading under the caret, not the nearest division.
|
||
assert.equal(compassMarkerSlot(350, 350), 0);
|
||
assert.equal(compassMarkerSlot(35, 350), 1.5);
|
||
assert.equal(compassMarkerSlot(260, 350), -3);
|
||
assert.equal(compassMarkerSlot(80, 350), 3);
|
||
assert.equal(compassMarkerSlot(90, 350), null);
|
||
assert.equal(compassMarkerSlot(180, 350), null);
|
||
assert.equal(compassMarkerSlot(Number.NaN, 0), null);
|
||
});
|
||
|
||
test('a scrolling tape puts the exact heading under the caret', () => {
|
||
// At 100° the divisions sit a third of a slot off their snapped places,
|
||
// so the bow mark (also 100°) lines up with the caret, not 10° beside it.
|
||
const tape = scrollingCompassDivisions(100);
|
||
assert.deepEqual(
|
||
tape.map(({ heading }) => heading),
|
||
[330, 0, 30, 60, 90, 120, 150, 180, 210],
|
||
);
|
||
const nearest = tape.find((division) => division.nearest);
|
||
assert.equal(nearest.heading, 90);
|
||
assert.equal(nearest.slot, -0.3333);
|
||
assert.equal(tape.find(({ heading }) => heading === 120).slot, 0.6667);
|
||
assert.equal(compassMarkerSlot(100, 100), 0);
|
||
assert.equal(compassMarkerSlot(90, 100), nearest.slot);
|
||
// Across north the slots stay continuous.
|
||
assert.deepEqual(
|
||
scrollingCompassDivisions(355, 1).map(({ heading, slot }) => [
|
||
heading,
|
||
slot,
|
||
]),
|
||
[
|
||
[330, -0.8333],
|
||
[0, 0.1667],
|
||
[30, 1.1667],
|
||
],
|
||
);
|
||
assert.deepEqual(scrollingCompassDivisions(Number.NaN), []);
|
||
});
|
||
|
||
test('compass labels use cardinals where exact and degrees elsewhere', () => {
|
||
assert.equal(formatCompassDivision(270), 'W');
|
||
assert.equal(formatCompassDivision(30), '030');
|
||
});
|
||
|
||
test('a heading that rounds to north reads 000, not 360', () => {
|
||
assert.equal(formatHeadingReadout(359.6), '000');
|
||
assert.equal(formatHeadingReadout(359.4), '359');
|
||
assert.equal(formatHeadingReadout(0), '000');
|
||
assert.equal(formatHeadingReadout(10.2), '010');
|
||
assert.equal(formatHeadingReadout(-0.6), '359');
|
||
assert.equal(formatHeadingReadout(Number.NaN), '---');
|
||
});
|
||
|
||
test('great-circle bearing resolves cardinal directions', () => {
|
||
assert.ok(Math.abs(bearingBetweenCoordinates(0, 0, 1, 0) - 0) < 0.001);
|
||
assert.ok(Math.abs(bearingBetweenCoordinates(0, 0, 0, 1) - 90) < 0.001);
|
||
assert.equal(bearingBetweenCoordinates(0, 0, 0, 0), null);
|
||
});
|
||
|
||
test('relative bearing stays on the shortest signed arc', () => {
|
||
assert.equal(relativeBearing(10, 350), 20);
|
||
assert.equal(relativeBearing(350, 10), -20);
|
||
assert.equal(relativeBearing(180, 0), -180);
|
||
assert.equal(relativeBearing(null, 0), null);
|
||
});
|
||
|
||
test('HUD rail layout centers between intersecting upper and lower obstacles', () => {
|
||
assert.deepEqual(resolveHudRailLayout({
|
||
viewportHeight: 1000,
|
||
panelHeight: 200,
|
||
laneLeft: 50,
|
||
laneRight: 410,
|
||
baseTop: 280,
|
||
baseBottom: 940,
|
||
gap: 10,
|
||
obstacles: [
|
||
{ left: 40, right: 240, top: 320, bottom: 460 },
|
||
{ left: 300, right: 1000, top: 780, bottom: 930 },
|
||
{ left: 900, right: 1100, top: 300, bottom: 700 },
|
||
],
|
||
}), {
|
||
top: 520,
|
||
maxHeight: 300,
|
||
safeTop: 470,
|
||
safeBottom: 770,
|
||
constrained: false,
|
||
});
|
||
});
|
||
|
||
test('HUD rail layout constrains an oversized panel to the safe corridor', () => {
|
||
const layout = resolveHudRailLayout({
|
||
viewportHeight: 800,
|
||
panelHeight: 300,
|
||
laneLeft: 20,
|
||
laneRight: 340,
|
||
baseTop: 250,
|
||
baseBottom: 700,
|
||
obstacles: [{ left: 0, right: 350, top: 500, bottom: 760 }],
|
||
});
|
||
assert.equal(layout.top, 250);
|
||
assert.equal(layout.maxHeight, 238);
|
||
assert.equal(layout.constrained, true);
|
||
});
|
||
|
||
test('HUD rail layout can anchor controls to the start of the safe corridor', () => {
|
||
const layout = resolveHudRailLayout({
|
||
viewportHeight: 1000,
|
||
panelHeight: 100,
|
||
laneLeft: 900,
|
||
laneRight: 1250,
|
||
baseTop: 260,
|
||
baseBottom: 940,
|
||
align: 'start',
|
||
});
|
||
assert.equal(layout.top, 260);
|
||
assert.equal(layout.safeTop, 260);
|
||
assert.equal(layout.safeBottom, 940);
|
||
});
|
||
|
||
// --- Contact panel readout (Monday recording bug: NEXT onto a non-aircraft) ---
|
||
|
||
const TRACKED_INFO = Object.freeze({
|
||
layerId: 'flights',
|
||
icao24: 'aaa077',
|
||
latitude: 30.2,
|
||
longitude: -97.7,
|
||
});
|
||
|
||
function contextSnapshot(subject, extra = {}) {
|
||
return {
|
||
subject,
|
||
evaluatedAt: 1_700_000_000_000,
|
||
radiusM: 250_000,
|
||
cohorts: [],
|
||
navigation: { canPrevious: true, canNext: true },
|
||
...extra,
|
||
};
|
||
}
|
||
|
||
test('Contact panel hides only when there is no snapshot at all', () => {
|
||
const readout = resolveCockpitContextReadout({ snapshot: null, info: TRACKED_INFO });
|
||
assert.equal(readout.visible, false);
|
||
assert.equal(readout.mode, 'standby');
|
||
assert.equal(
|
||
resolveCockpitContextReadout({ info: TRACKED_INFO }).visible,
|
||
false,
|
||
'a missing snapshot argument is the same standby case',
|
||
);
|
||
});
|
||
|
||
test('Contact panel survives NEXT onto a vessel or installation subject', () => {
|
||
// The panel owns the NEXT button. Hiding it because the subject is not the
|
||
// tracked aircraft strands the operator with no way back (owner hit this on
|
||
// camera: "click next... whole left panel disappears").
|
||
for (const subject of [
|
||
{ layerId: 'ais-live-vessels', id: '353136000', label: 'MAERSK DETROIT' },
|
||
{ layerId: 'military-installations', id: 'fort-hood', label: 'FORT CAVAZOS' },
|
||
{ layerId: 'military', id: 'ae01ce', label: 'RCH451' },
|
||
]) {
|
||
const readout = resolveCockpitContextReadout({
|
||
snapshot: contextSnapshot(subject),
|
||
info: TRACKED_INFO,
|
||
});
|
||
assert.equal(readout.visible, true, `${subject.layerId} subject must keep the panel up`);
|
||
assert.equal(readout.mode, 'foreign');
|
||
assert.equal(readout.subjectMatchesTracked, false);
|
||
}
|
||
});
|
||
|
||
test('a foreign subject dashes the aircraft-relative fields and keeps the rest live', () => {
|
||
const readout = resolveCockpitContextReadout({
|
||
snapshot: contextSnapshot({ layerId: 'ais-live-vessels', id: '353136000', label: 'MAERSK DETROIT' }),
|
||
info: TRACKED_INFO,
|
||
});
|
||
assert.equal(readout.visible, true);
|
||
assert.equal(readout.mode, 'foreign');
|
||
// Nose-relative direction/bearing are measured in the tracked aircraft's own
|
||
// frame while every other row value is measured from the subject; rendering
|
||
// both live would present one mixed-frame reading as a single measurement.
|
||
assert.equal(readout.aircraftRelative, false);
|
||
assert.equal(readout.contactLost, false, 'subject-frame values still refresh');
|
||
});
|
||
|
||
test('the tracked aircraft as subject keeps every field in its own frame', () => {
|
||
const readout = resolveCockpitContextReadout({
|
||
snapshot: contextSnapshot({ layerId: 'flights', id: 'aaa077', label: 'SWA1234' }),
|
||
info: TRACKED_INFO,
|
||
});
|
||
assert.equal(readout.visible, true);
|
||
assert.equal(readout.mode, 'tracked');
|
||
assert.equal(readout.subjectMatchesTracked, true);
|
||
assert.equal(readout.aircraftRelative, true);
|
||
assert.equal(readout.contactLost, false);
|
||
});
|
||
|
||
test('a fast-culled subject keeps the panel up as CONTACT LOST instead of collapsing it', () => {
|
||
for (const subject of [
|
||
{ layerId: 'flights', id: 'aaa077', label: 'SWA1234' },
|
||
{ layerId: 'ais-live-vessels', id: '353136000', label: 'MAERSK DETROIT' },
|
||
]) {
|
||
const readout = resolveCockpitContextReadout({
|
||
snapshot: contextSnapshot(subject, { subjectPresent: false }),
|
||
info: TRACKED_INFO,
|
||
});
|
||
assert.equal(readout.visible, true, 'a lost contact must not take the panel down');
|
||
assert.equal(readout.mode, 'lost');
|
||
assert.equal(readout.contactLost, true, 'last-known values stay on screen');
|
||
}
|
||
});
|
||
|
||
test('an explicitly present subject is never reported lost', () => {
|
||
const present = resolveCockpitContextReadout({
|
||
snapshot: contextSnapshot({ layerId: 'flights', id: 'aaa077' }, { subjectPresent: true }),
|
||
info: TRACKED_INFO,
|
||
});
|
||
assert.equal(present.contactLost, false);
|
||
assert.equal(present.mode, 'tracked');
|
||
// Snapshots from before the presence field existed must not read as lost.
|
||
const legacy = resolveCockpitContextReadout({
|
||
snapshot: contextSnapshot({ layerId: 'flights', id: 'aaa077' }),
|
||
info: TRACKED_INFO,
|
||
});
|
||
assert.equal(legacy.contactLost, false);
|
||
});
|
||
|
||
test('the tracked flight layer is resolved by normalized tracked identity', () => {
|
||
const civilian = { icao24: 'aaa077', callsign: 'SWA1234' };
|
||
const military = { icao24: 'ae01ce', callsign: 'RCH451' };
|
||
// Both layers describe a tracked aircraft during a cross-layer handoff;
|
||
// civilian-first precedence would hand the cockpit the wrong aircraft.
|
||
assert.equal(
|
||
resolveTrackedAircraftInfo({ civilian, military, trackedId: 'military:ae01ce' }).layerId,
|
||
'military',
|
||
);
|
||
assert.equal(
|
||
resolveTrackedAircraftInfo({ civilian, military, trackedId: 'military:ae01ce' }).icao24,
|
||
'ae01ce',
|
||
);
|
||
assert.equal(
|
||
resolveTrackedAircraftInfo({ civilian, military, trackedId: 'flights:aaa077' }).layerId,
|
||
'flights',
|
||
);
|
||
// Case-insensitive: layers stamp lowercase hex, callers may not.
|
||
assert.equal(
|
||
resolveTrackedAircraftInfo({ civilian, military, trackedId: 'MILITARY:AE01CE' }).layerId,
|
||
'military',
|
||
);
|
||
});
|
||
|
||
test('tracked-identity resolution falls back to layer precedence', () => {
|
||
const civilian = { icao24: 'aaa077' };
|
||
const military = { icao24: 'ae01ce' };
|
||
// No stamped identity (or one that matches neither layer) keeps the historic
|
||
// civilian-first order so tracking paths without gevTrackedId still work.
|
||
assert.equal(resolveTrackedAircraftInfo({ civilian, military }).layerId, 'flights');
|
||
assert.equal(
|
||
resolveTrackedAircraftInfo({ civilian, military, trackedId: 'satellites:25544' }).layerId,
|
||
'flights',
|
||
);
|
||
assert.equal(resolveTrackedAircraftInfo({ military, trackedId: '' }).layerId, 'military');
|
||
assert.equal(resolveTrackedAircraftInfo({ trackedId: 'flights:aaa077' }), null);
|
||
assert.equal(resolveTrackedAircraftInfo(), null);
|
||
});
|
||
|
||
test('cockpit emergency wording states the broadcast code, not a conclusion', () => {
|
||
assert.equal(
|
||
formatCockpitEmergency({ label: 'General emergency', squawk: '7700', source: 'squawk', severity: 'emergency' }),
|
||
'SQUAWK 7700 · GENERAL EMERGENCY',
|
||
);
|
||
assert.equal(
|
||
formatCockpitEmergency({ label: 'Downed aircraft', squawk: '1200', source: 'ads-b', severity: 'emergency' }),
|
||
'ADS-B EMERGENCY · DOWNED AIRCRAFT',
|
||
'an unrelated squawk is not quoted beside an ADS-B status',
|
||
);
|
||
assert.equal(
|
||
formatCockpitEmergency({ label: 'Minimum fuel', squawk: null, source: 'ads-b', severity: 'priority' }),
|
||
'ADS-B PRIORITY · MINIMUM FUEL',
|
||
);
|
||
assert.equal(formatCockpitEmergency(null), null);
|
||
assert.equal(formatCockpitEmergency({ label: '' }), null);
|
||
});
|
||
|
||
test('an application subject’s ground lift eases its clearance, never the floor', () => {
|
||
// On entry the lift asked for applies at once.
|
||
assert.equal(easeCockpitLift(null, 1.5, 0, 0.016), 1.5);
|
||
// A floor cell that rises by a metre is met at the lift rate...
|
||
let lift = 0;
|
||
const frames = [];
|
||
for (let i = 0; i < 60; i += 1) {
|
||
lift = easeCockpitLift(lift, 1, 0, 0.016);
|
||
frames.push(lift);
|
||
}
|
||
assert.ok(frames[0] <= COCKPIT_SUBJECT_LIFT_MPS * 0.016 + 1e-9);
|
||
for (let i = 1; i < frames.length; i += 1)
|
||
assert.ok(
|
||
frames[i] - frames[i - 1] <= COCKPIT_SUBJECT_LIFT_MPS * 0.016 + 1e-9,
|
||
);
|
||
assert.ok(Math.abs(frames.at(-1) - 1) < 1e-9, 'reached in under a second');
|
||
// ...and given back the same way.
|
||
assert.ok(Math.abs(easeCockpitLift(1, 0, 0, 0.1) - 0.85) < 1e-9);
|
||
// What keeps the eye out of the floor itself is never eased.
|
||
assert.equal(easeCockpitLift(0, 201.5, 200, 0.016), 200);
|
||
assert.equal(
|
||
easeCockpitLift(0.2, -3, -3, 0.016),
|
||
0.2 - COCKPIT_SUBJECT_LIFT_MPS * 0.016,
|
||
);
|
||
assert.equal(easeCockpitLift(0, Number.NaN, Number.NaN, 0.016), 0);
|
||
});
|