132 lines
5.1 KiB
TypeScript
132 lines
5.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import { cargoWeightFor, loadEligiblePlanes } from './RightPane';
|
||
import type { PackageState, PlaneState, StateSnapshot } from '@/lib/game/state';
|
||
|
||
// v0.12.0 (weight model v0.27.0) — the package-model picker rules: a plane can LOAD a
|
||
// package only while it is GROUNDED at the package's origin with enough payload room for
|
||
// it (maxPayloadKg from the snapshot catalog; expired packages still occupy theirs). The
|
||
// Load button acts directly on the single eligible plane, or shows a picker when several qualify.
|
||
|
||
const CATALOG = [
|
||
{ model: 'atr72', label: 'ATR 72-600', price: 200_000, maxFuel: 6300, burnPerKm: 1.75, emptyWeightKg: 9_300, speedKmh: 510, maxPayloadKg: 4_800 },
|
||
];
|
||
|
||
function plane(over: Partial<PlaneState> & Pick<PlaneState, 'id'>): PlaneState {
|
||
return {
|
||
model: 'atr72',
|
||
status: 'available',
|
||
fuel: 100,
|
||
maxFuel: 100,
|
||
locationIcao: 'EDDF',
|
||
locationName: 'Frankfurt Main',
|
||
destIcao: null,
|
||
destName: null,
|
||
departureSimTime: null,
|
||
lat: 50,
|
||
lon: 8.5,
|
||
health: 100, // v0.39.0 (ISS-069) — full-health fixture default
|
||
repairUntilSimTime: null,
|
||
queue: [],
|
||
...over,
|
||
};
|
||
}
|
||
|
||
function pkg(over: Partial<PackageState> & Pick<PackageState, 'id'>): PackageState {
|
||
return {
|
||
originIcao: 'EDDF',
|
||
originName: 'Frankfurt Main',
|
||
destIcao: 'LEMD',
|
||
destName: 'Madrid Barajas',
|
||
reward: 1000,
|
||
cargoType: 'mail',
|
||
cargoCount: 4,
|
||
weightKg: 1_000,
|
||
special: null, // v0.37.0 (ISS-073) — no special status by default in fixtures
|
||
distanceKm: 1839, // v0.48.0 (ISS-095) — EDDF→LEMD great-circle distance
|
||
deadlineSimTime: 1_000_000,
|
||
status: 'open',
|
||
planeId: null,
|
||
createdSimTime: 0,
|
||
...over,
|
||
};
|
||
}
|
||
|
||
function snap(planes: PlaneState[], packages: PackageState[] = []): StateSnapshot {
|
||
return {
|
||
account: { cash: 0, reputation: 0 },
|
||
simState: { clockMs: 0, timeScale: 1, paused: false },
|
||
planes,
|
||
packages,
|
||
catalog: CATALOG,
|
||
fuelPricePerL: 0.8,
|
||
sellFraction: 0.5,
|
||
};
|
||
}
|
||
|
||
describe('cargoWeightFor', () => {
|
||
it('sums the weight of loaded AND expired packages bound to the plane (both occupy payload)', () => {
|
||
const s = snap(
|
||
[plane({ id: 'p1' })],
|
||
[
|
||
pkg({ id: 'a', status: 'loaded', planeId: 'p1', weightKg: 800 }),
|
||
pkg({ id: 'b', status: 'expired', planeId: 'p1', weightKg: 500 }),
|
||
pkg({ id: 'c', status: 'open', planeId: null, weightKg: 9_000 }), // not on any plane
|
||
pkg({ id: 'd', status: 'delivered', planeId: 'p1', weightKg: 700 }), // terminal — payload long freed
|
||
],
|
||
);
|
||
expect(cargoWeightFor(s, 'p1')).toBe(1_300);
|
||
});
|
||
|
||
it('is 0 for a plane with no bound cargo', () => {
|
||
expect(cargoWeightFor(snap([plane({ id: 'p1' })]), 'p1')).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('loadEligiblePlanes (Load button eligibility)', () => {
|
||
it('includes only grounded planes at the package origin with enough payload room', () => {
|
||
const s = snap([
|
||
plane({ id: 'at-origin' }), // eligible
|
||
plane({ id: 'elsewhere', locationIcao: 'LEMD' }), // wrong airport
|
||
plane({ id: 'in-flight', status: 'flying', destIcao: 'LEMD', destName: 'Madrid Barajas' }), // flying
|
||
]);
|
||
expect(loadEligiblePlanes(s, pkg({ id: 'x' })).map((p) => p.id)).toEqual(['at-origin']);
|
||
});
|
||
|
||
it('a plane with partial payload can still load a package that fits', () => {
|
||
const s = snap([plane({ id: 'partial' })], [pkg({ id: 'onboard', status: 'loaded', planeId: 'partial', weightKg: 3_000 })]);
|
||
expect(loadEligiblePlanes(s, pkg({ id: 'x' })).map((p) => p.id)).toEqual(['partial']); // 3000 + 1000 ≤ 4800
|
||
});
|
||
|
||
it('excludes a plane whose payload is full (maxPayloadKg from the catalog)', () => {
|
||
const packages = Array.from({ length: 4 }, (_, i) =>
|
||
pkg({ id: `fill-${i}`, status: 'loaded', planeId: 'full', weightKg: 1_200 }), // 4 × 1200 = 4800
|
||
);
|
||
const s = snap([plane({ id: 'full' })], packages);
|
||
expect(loadEligiblePlanes(s, pkg({ id: 'x' }))).toEqual([]);
|
||
});
|
||
|
||
it('a plane at its limit with an expired package still counts as full', () => {
|
||
const packages = [
|
||
...Array.from({ length: 4 }, (_, i) => pkg({ id: `fill-${i}`, status: 'loaded', planeId: 'full', weightKg: 960 })),
|
||
pkg({ id: 'exp', status: 'expired', planeId: 'full', weightKg: 960 }), // occupies its payload — 5 × 960 = 4800
|
||
];
|
||
const s = snap([plane({ id: 'full' })], packages);
|
||
expect(loadEligiblePlanes(s, pkg({ id: 'x' }))).toEqual([]);
|
||
});
|
||
|
||
it('returns several planes when several qualify (picker case)', () => {
|
||
const s = snap([plane({ id: 'a' }), plane({ id: 'b' })]);
|
||
expect(loadEligiblePlanes(s, pkg({ id: 'x' })).map((p) => p.id).sort()).toEqual(['a', 'b']);
|
||
});
|
||
|
||
it('returns nothing when no plane is at the origin (Load stays disabled)', () => {
|
||
const s = snap([plane({ id: 'far', locationIcao: 'LEMD' })]);
|
||
expect(loadEligiblePlanes(s, pkg({ id: 'x' }))).toEqual([]);
|
||
});
|
||
|
||
it('a plane with an unknown model has no maxPayloadKg in the catalog → never eligible', () => {
|
||
const s = snap([plane({ id: 'mystery', model: 'mystery' })]);
|
||
expect(loadEligiblePlanes(s, pkg({ id: 'x' }))).toEqual([]);
|
||
});
|
||
});
|