1
0
This repository has been archived on 2026-08-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tera/src/test/sceneAircraft.test.ts
T

131 lines
5.5 KiB
TypeScript

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> = {}): 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);
});
});