import assert from "node:assert/strict"; import { describe, it } from "node:test"; import * as THREE from "three"; import { CALIFORNIA_AIR_ROUTE, createSceneAircraft, type SceneAircraftOptions, } from "../aircraft/index.ts"; function options(overrides: Partial = {}): SceneAircraftOptions { return { project: (lat, lng) => [(lng + 121) * 20, -(lat - 36) * 20], groundAt: () => 2, route: CALIFORNIA_AIR_ROUTE, initialPosition: { lat: 34.0522, lng: -118.2437 }, initialAltitudeM: 1_000, initialHeadingDeg: 0, fixedStepSeconds: 0.1, altitudeSceneUnitsPerMetre: 0.005, visualSceneUnitsPerMetre: 0.1, ...overrides, }; } describe("playable scene aircraft", () => { it("projects a stable root and stays inert until activated", () => { const aircraft = createSceneAircraft(options()); const root = aircraft.root; assert.equal(root.name, "playable-scene-aircraft"); assert.ok(root.getObjectByName("electric-aircraft")); assert.deepEqual(root.position.toArray(), [( -118.2437 + 121) * 20, 7, -(34.0522 - 36) * 20]); assert.equal(root.scale.x, 0.1); aircraft.setActions({ throttle: 1, roll: 0.8 }); aircraft.tick(0.2); assert.equal(aircraft.state().elapsedSteps, 0); assert.equal(aircraft.root, root); aircraft.setActive(true); assert.equal(aircraft.actions().throttle, 0, "activation clears stale input"); aircraft.setActions({ throttle: 1, roll: 0.8 }); aircraft.tick(0.2); assert.equal(aircraft.state().elapsedSteps, 2); assert.equal(aircraft.state().mode, "manual"); assert.equal(aircraft.view.position, aircraft.view.position); assert.deepEqual(aircraft.view.position.toArray(), root.position.toArray()); aircraft.dispose(); }); it("maps heading, pitch, and right-bank roll under Tera's -Z convention", () => { const aircraft = createSceneAircraft(options({ active: true })); aircraft.setActions({ throttle: 0.7, pitch: 1, roll: 1, yaw: 0.2 }); aircraft.tick(1); const state = aircraft.state(); assert.equal(aircraft.root.rotation.order, "YXZ"); assert.ok(Math.abs(aircraft.root.rotation.x - state.pitchDeg * Math.PI / 180) < 1e-12); assert.ok(Math.abs(aircraft.root.rotation.y + state.headingDeg * Math.PI / 180) < 1e-12); assert.ok(Math.abs(aircraft.root.rotation.z + state.rollDeg * Math.PI / 180) < 1e-12); assert.ok(aircraft.root.rotation.z < 0, "positive controller roll presents right wing down"); aircraft.dispose(); }); it("animates articulated surfaces and fans from authoritative state", () => { const aircraft = createSceneAircraft(options({ active: true })); const leftAileron = aircraft.root.getObjectByName("electric-aircraft.aileron-left"); const rightAileron = aircraft.root.getObjectByName("electric-aircraft.aileron-right"); const leftFan = aircraft.root.getObjectByName("electric-aircraft.fan-left"); assert.ok(leftAileron && rightAileron && leftFan); aircraft.setActions({ throttle: 1, roll: 0.8, pitch: 0.4, yaw: -0.3 }); aircraft.tick(0.5); assert.ok(leftAileron.rotation.x > 0); assert.ok(rightAileron.rotation.x < 0); assert.notEqual(leftFan.rotation.z, 0); aircraft.dispose(); }); it("publishes defensive finite chase poses for north and east headings", () => { const north = createSceneAircraft(options({ initialHeadingDeg: 0 })); const northPose = north.followPose(); assert.ok(northPose.position.z > north.root.position.z, "camera is south/behind northbound flight"); assert.ok(northPose.target.z < north.root.position.z); northPose.position.x = 999; assert.notEqual(north.followPose().position.x, 999); north.dispose(); const east = createSceneAircraft(options({ initialHeadingDeg: 90 })); const eastPose = east.followPose(); assert.ok(eastPose.position.x < east.root.position.x); assert.ok(eastPose.target.x > east.root.position.x); assert.ok(eastPose.position.toArray().every(Number.isFinite)); assert.ok(eastPose.target.toArray().every(Number.isFinite)); east.dispose(); }); it("clears edge actions, resumes assistance, and resets controller plus rig", () => { const aircraft = createSceneAircraft(options({ active: true })); const spawn = aircraft.state(); aircraft.setActions({ throttle: 1, roll: 1, modeRequest: "manual", reset: false }); aircraft.tick(0.2); assert.equal(aircraft.actions().modeRequest, "none"); assert.equal(aircraft.actions().throttle, 1); aircraft.setActions({ modeRequest: "assisted" }); aircraft.tick(0.1); assert.equal(aircraft.state().mode, "assisted"); aircraft.reset(); assert.deepEqual(aircraft.state(), spawn); assert.deepEqual(aircraft.actions(), { throttle: 0, yaw: 0, pitch: 0, roll: 0, modeRequest: "none", reset: false, }); aircraft.dispose(); }); it("disposes idempotently and refuses invalid projection/scaling", () => { const aircraft = createSceneAircraft(options()); const parent = new THREE.Group(); parent.add(aircraft.root); aircraft.dispose(); aircraft.dispose(); assert.equal(aircraft.root.parent, null); const before = aircraft.state(); aircraft.tick(1); assert.deepEqual(aircraft.state(), before); assert.throws(() => createSceneAircraft(options({ visualSceneUnitsPerMetre: 0 })), RangeError); assert.throws(() => createSceneAircraft(options({ project: () => [Infinity, 0] })), RangeError); assert.throws(() => createSceneAircraft(options({ groundAt: () => Number.NaN })), RangeError); }); });