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:
+48
-9
@@ -9,8 +9,9 @@ import {
|
||||
generatePackage,
|
||||
HUB_ICAO,
|
||||
mulberry32,
|
||||
REWARD_PER_KG,
|
||||
REWARD_PER_KM,
|
||||
rewardForDistanceKm,
|
||||
rewardFor,
|
||||
STARTING_CASH,
|
||||
STARTING_REPUTATION,
|
||||
type SeedAirport,
|
||||
@@ -70,12 +71,26 @@ describe('buildSeed', () => {
|
||||
expect(new Set(seed.packages.map((c) => c.id)).size).toBe(seed.packages.length);
|
||||
});
|
||||
|
||||
it('scales reward by haversine distance: round(500 + 2.5 * km)', () => {
|
||||
it('applies the reward formula: round(500 + 2.5 * km + 0.5 * weightKg)', () => {
|
||||
const seed = seedWith(42);
|
||||
for (const c of seed.packages) {
|
||||
const o = AIRPORTS.find((a) => a.icao === c.originIcao)!;
|
||||
const d = AIRPORTS.find((a) => a.icao === c.destIcao)!;
|
||||
expect(c.reward).toBe(Math.round(BASE_REWARD + REWARD_PER_KM * haversineKm(o.lat, o.lon, d.lat, d.lon)));
|
||||
expect(c.reward).toBe(
|
||||
Math.round(BASE_REWARD + REWARD_PER_KM * haversineKm(o.lat, o.lon, d.lat, d.lon) + REWARD_PER_KG * c.weightKg),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('gives every package a positive integer weight that fits the starter plane alone (balance constraint)', () => {
|
||||
const maxPayload = PLANE_TYPES[STARTER_MODEL].maxPayloadKg;
|
||||
for (const seed of [seedWith(42), seedWith(7)]) {
|
||||
for (const c of seed.packages) {
|
||||
expect(Number.isInteger(c.weightKg)).toBe(true);
|
||||
expect(c.weightKg).toBeGreaterThan(0);
|
||||
// max count × max jitter (1.3×) must still fit the ATR alone — see UNIT_WEIGHT_KG.
|
||||
expect(c.weightKg).toBeLessThanOrEqual(maxPayload);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -106,9 +121,9 @@ describe('buildSeed', () => {
|
||||
expect(() => buildSeed({ airports: AIRPORTS.slice(0, 1), rng: mulberry32(42), nowMs: NOW })).toThrow();
|
||||
});
|
||||
|
||||
it('rewardForDistanceKm applies the documented formula', () => {
|
||||
expect(rewardForDistanceKm(0)).toBe(BASE_REWARD);
|
||||
expect(rewardForDistanceKm(1000)).toBe(Math.round(BASE_REWARD + REWARD_PER_KM * 1000));
|
||||
it('rewardFor applies the documented formula', () => {
|
||||
expect(rewardFor(0, 0)).toBe(BASE_REWARD);
|
||||
expect(rewardFor(1000, 2_000)).toBe(Math.round(BASE_REWARD + REWARD_PER_KM * 1000 + REWARD_PER_KG * 2_000));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -127,7 +142,9 @@ describe('generatePackage (shared seed/spawn builder)', () => {
|
||||
|
||||
const o = AIRPORTS.find((a) => a.icao === c.originIcao)!;
|
||||
const d = AIRPORTS.find((a) => a.icao === c.destIcao)!;
|
||||
expect(c.reward).toBe(Math.round(BASE_REWARD + REWARD_PER_KM * haversineKm(o.lat, o.lon, d.lat, d.lon)));
|
||||
expect(c.reward).toBe(
|
||||
Math.round(BASE_REWARD + REWARD_PER_KM * haversineKm(o.lat, o.lon, d.lat, d.lon) + REWARD_PER_KG * c.weightKg),
|
||||
);
|
||||
|
||||
// Same 6–72h game-time deadline window as the seed.
|
||||
expect(c.deadlineSimTime).toBeGreaterThan(NOW + 6 * 3_600_000 - 1);
|
||||
@@ -173,7 +190,9 @@ describe('generatePackage (shared seed/spawn builder)', () => {
|
||||
|
||||
const o = AIRPORTS.find((a) => a.icao === c.originIcao)!;
|
||||
const d = AIRPORTS.find((a) => a.icao === c.destIcao)!;
|
||||
expect(c.reward).toBe(Math.round(BASE_REWARD + REWARD_PER_KM * haversineKm(o.lat, o.lon, d.lat, d.lon)));
|
||||
expect(c.reward).toBe(
|
||||
Math.round(BASE_REWARD + REWARD_PER_KM * haversineKm(o.lat, o.lon, d.lat, d.lon) + REWARD_PER_KG * c.weightKg),
|
||||
);
|
||||
});
|
||||
|
||||
it('destIcao forces the destination; origin stays random but distinct', () => {
|
||||
@@ -189,7 +208,9 @@ describe('generatePackage (shared seed/spawn builder)', () => {
|
||||
|
||||
const o = AIRPORTS[0]; // EDDF
|
||||
const d = AIRPORTS[1]; // EHAM
|
||||
expect(c.reward).toBe(Math.round(BASE_REWARD + REWARD_PER_KM * haversineKm(o.lat, o.lon, d.lat, d.lon)));
|
||||
expect(c.reward).toBe(
|
||||
Math.round(BASE_REWARD + REWARD_PER_KM * haversineKm(o.lat, o.lon, d.lat, d.lon) + REWARD_PER_KG * c.weightKg),
|
||||
);
|
||||
});
|
||||
|
||||
it('is deterministic for a fixed RNG with forced endpoints', () => {
|
||||
@@ -212,4 +233,22 @@ describe('generatePackage (shared seed/spawn builder)', () => {
|
||||
expect(a).toEqual(b);
|
||||
});
|
||||
});
|
||||
|
||||
describe('forced weight (v0.27.0 e2e hook)', () => {
|
||||
it('weightKg forces the package weight and feeds the payout', () => {
|
||||
const c = generatePackage({ id: 'c-w', airports: AIRPORTS, rng: mulberry32(42), nowMs: NOW, weightKg: 1_234 });
|
||||
expect(c.weightKg).toBe(1_234);
|
||||
|
||||
const o = AIRPORTS.find((a) => a.icao === c.originIcao)!;
|
||||
const d = AIRPORTS.find((a) => a.icao === c.destIcao)!;
|
||||
expect(c.reward).toBe(Math.round(BASE_REWARD + REWARD_PER_KM * haversineKm(o.lat, o.lon, d.lat, d.lon) + REWARD_PER_KG * 1_234));
|
||||
|
||||
// Deterministic for a fixed RNG…
|
||||
const again = generatePackage({ id: 'c-w', airports: AIRPORTS, rng: mulberry32(42), nowMs: NOW, weightKg: 1_234 });
|
||||
expect(again).toEqual(c);
|
||||
// …and the forced value consumes no RNG draw — the deadline lands where an unforced call's jitter would.
|
||||
const unforced = generatePackage({ id: 'c-u', airports: AIRPORTS, rng: mulberry32(42), nowMs: NOW });
|
||||
expect(c.deadlineSimTime).not.toBe(unforced.deadlineSimTime);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user