Package weight, payload capacity and per-kg payout (ISS-045)
CI / build + unit + e2e (pull_request) Successful in 8m2s
CI / build + unit + e2e (pull_request) Successful in 8m2s
Packages carry a real weightKg (unit weight x count with +/-30% jitter), plane capacity becomes maxPayloadKg instead of 6 slots, load validation sums cargo weight against the payload, and the payout gains a per-kg part. Loadout display shows tonnage; OpenAPI swaps CatalogEntry.capacity for maxPayloadKg and adds PackageState.weightKg.
This commit is contained in:
+16
-7
@@ -53,8 +53,16 @@ async function getState(page: Page): Promise<StateBody> {
|
||||
}
|
||||
|
||||
// The e2e instance runs with FLIGHT_TEST_HOOKS=1 (docker-compose.yml) — the hook is live.
|
||||
async function createPackage(page: Page, from?: string, to?: string, deadlineOffsetMs?: number): Promise<StateBody['packages'][number]> {
|
||||
const res = await page.request.post('/api/test/create-package', { data: { originIcao: from, destIcao: to, deadlineOffsetMs } });
|
||||
// weightKg (v0.27.0) forces the package weight instead of the RNG draw, so flows that load
|
||||
// several packages onto one plane stay under its payload deterministically.
|
||||
async function createPackage(
|
||||
page: Page,
|
||||
from?: string,
|
||||
to?: string,
|
||||
deadlineOffsetMs?: number,
|
||||
weightKg?: number,
|
||||
): Promise<StateBody['packages'][number]> {
|
||||
const res = await page.request.post('/api/test/create-package', { data: { originIcao: from, destIcao: to, deadlineOffsetMs, weightKg } });
|
||||
expect(res.status()).toBe(201);
|
||||
return (await res.json()) as StateBody['packages'][number];
|
||||
}
|
||||
@@ -240,9 +248,10 @@ test('abandon while grounded deducts exactly the formula penalty', async ({ page
|
||||
await flyHomeViaApi(page);
|
||||
const before = await getState(page);
|
||||
|
||||
// Two packages at the hub — load both, abandon one through the UI.
|
||||
const pkgA = await createPackage(page, HUB, 'LEMD', 24 * 3_600_000);
|
||||
const pkgB = await createPackage(page, HUB, 'LEMD', 24 * 3_600_000);
|
||||
// Two packages at the hub — load both, abandon one through the UI. Forced weights keep
|
||||
// the pair under the starter's payload (v0.27.0) so both loads are deterministic.
|
||||
const pkgA = await createPackage(page, HUB, 'LEMD', 24 * 3_600_000, 1_000);
|
||||
const pkgB = await createPackage(page, HUB, 'LEMD', 24 * 3_600_000, 1_000);
|
||||
for (const p of [pkgA, pkgB]) {
|
||||
const res = await page.request.post(`/api/packages/${p.id}/load`, { data: { planeId: 'plane-starter' } });
|
||||
expect(res.ok()).toBeTruthy();
|
||||
@@ -282,8 +291,8 @@ test('expired packages deliver and abandon with no reward and no penalty', async
|
||||
// destination differs from A's so it stays bound on the plane at LEMD arrival and can
|
||||
// still be abandoned (an expired package whose dest matches the arrival airport is
|
||||
// unbound by the tick instead).
|
||||
const pkgA = await createPackage(page, HUB, 'LEMD', 10_000); // short game-time deadline
|
||||
const pkgB = await createPackage(page, HUB, 'EHAM', 10_000);
|
||||
const pkgA = await createPackage(page, HUB, 'LEMD', 10_000, 1_000); // short game-time deadline; forced weights keep the pair under payload (v0.27.0)
|
||||
const pkgB = await createPackage(page, HUB, 'EHAM', 10_000, 1_000);
|
||||
for (const p of [pkgA, pkgB]) {
|
||||
const res = await page.request.post(`/api/packages/${p.id}/load`, { data: { planeId: 'plane-starter' } });
|
||||
expect(res.ok()).toBeTruthy();
|
||||
|
||||
+2
-2
@@ -62,10 +62,10 @@ test('left pane renders wallet, reputation and the starter fleet at Frankfurt',
|
||||
await expect(row).toContainText(truth.locationName);
|
||||
}
|
||||
|
||||
// v0.12.0 loadout: "n/capacity pkgs" (capacity from the snapshot catalog — ATR 72-600 = 6).
|
||||
// v0.27.0 loadout: "X.Xt / Y.Yt" tonnage (used/max payload from the snapshot catalog — ATR 72-600 = 4.8t).
|
||||
const loadout = page.getByTestId('plane-loadout-plane-starter');
|
||||
await expect(loadout).toBeVisible();
|
||||
expect((await loadout.textContent()) ?? '').toMatch(/^\d+\/6 pkgs$/);
|
||||
expect((await loadout.textContent()) ?? '').toMatch(/^\d+\.\dt \/ \d+\.\dt$/);
|
||||
|
||||
// Sim clock renders (UTC wall-clock of the game time).
|
||||
await expect(page.getByTestId('sim-clock')).toContainText(/UTC/);
|
||||
|
||||
@@ -42,7 +42,7 @@ interface CatalogEntry {
|
||||
burnPerKm: number;
|
||||
emptyWeightKg: number; // v0.16.0 weighted burn input
|
||||
speedKmh: number;
|
||||
capacity: number;
|
||||
maxPayloadKg: number; // v0.27.0 — replaces the v0.12.0 slot capacity
|
||||
}
|
||||
|
||||
interface StateBody {
|
||||
@@ -309,7 +309,7 @@ test('market buy/sell render with previews from the catalog in /api/state; left-
|
||||
const body = await getState(page); // authoritative catalog + economy constants (v0.10.0)
|
||||
expect(body.catalog.length).toBeGreaterThan(0);
|
||||
const entry = body.catalog.find((e) => e.model === 'atr72')!;
|
||||
expect(entry.capacity).toBe(6); // v0.12.0 slot capacity exposed in the snapshot
|
||||
expect(entry.maxPayloadKg).toBe(4_800); // ATR 72-600 max payload (lib/game/catalog.ts)
|
||||
|
||||
await page.goto('/');
|
||||
// Buy: the model picker lists the catalog entry and the price preview is its full price.
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ test('GET /api/state returns a seeded snapshot', async ({ page }) => {
|
||||
|
||||
expect(Array.isArray(body.packages)).toBe(true);
|
||||
expect(body.packages.length).toBeGreaterThan(0);
|
||||
for (const key of ['id', 'originIcao', 'destIcao', 'reward', 'cargoType', 'deadlineSimTime', 'status']) {
|
||||
for (const key of ['id', 'originIcao', 'destIcao', 'reward', 'cargoType', 'weightKg', 'deadlineSimTime', 'status']) {
|
||||
expect(body.packages[0], `package.${key}`).toHaveProperty(key);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user