Step 10L.1: range factors, Lighter and Clipper
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e1ce7ee7d0
commit
faa23d2945
|
|
@ -1127,3 +1127,11 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
|||
|
||||
- **Renumbered on arrival:** titled "CR-009 … Phase 10I"; CR-009 to CR-011 and Phases 10I–10K already exist, so it is **CR-012**, done as **Phase 10L** (its 10I.n are 10L.n).
|
||||
- **Decisions:** the CR's 0.5.59–0.5.63 are **0.5.77–0.5.81**; its amended "0.5.49" and "0.5.50" are this plan's **0.5.67** and **0.5.68** (ship upgrades CR). Its "CR-006" is **CR-009**; "CR-008" is **CR-011**. Error code `would-strand`.
|
||||
|
||||
## Step 10L.1
|
||||
|
||||
- **New ship values** follow the CR's §2 descriptions: Lighter 42,400 cr (40 % of the way from Courier to Freighter), 1.6M km/h, 80 t (80 % of a Freighter), 2 h overhead, 0.012 h/t handling (40 % faster loading), 350 cr/M km; Clipper 61,600 cr (60 %), 6.5M km/h, 30 t (1.5× Courier), 2 h overhead, 600 cr/M km. Ranges (Lighter 10, Clipper 198) come from the measured leg distances; the reach table is enforced in 10L.4.
|
||||
- **"Clipper is the fastest model" includes the loaner** (6.0M km/h), so the Clipper is 6.5M km/h rather than just the fastest for sale.
|
||||
- **Handling time becomes per model where set** (`ShipModel.handlingGameHoursPerTonne`, optional, defaulting to the game-wide value); upgrades scale the model's own base.
|
||||
- **Roles are content** (`role`), one line per model including the loaner, for the Market (10L.7).
|
||||
- **The new sprites are listed in the manifest as pending** (`file: null`) so the "every ship has a sprite in the manifest" test holds; they use the placeholder until art arrives (10L.10).
|
||||
|
|
|
|||
|
|
@ -1,17 +1,61 @@
|
|||
{
|
||||
"sourceDir": "public/assets/map",
|
||||
"assets": {
|
||||
"bg": { "kind": "background", "file": "bg.png" },
|
||||
"sun": { "kind": "body", "file": "body-sun.png" },
|
||||
"body-earth": { "kind": "body", "file": "body-earth.png" },
|
||||
"body-moon": { "kind": "body", "file": "body-moon.png" },
|
||||
"body-mars": { "kind": "body", "file": "body-mars.png" },
|
||||
"body-ceres": { "kind": "body", "file": "body-ceres.png" },
|
||||
"body-jupiter": { "kind": "body", "file": "body-jupiter.png" },
|
||||
"badge-cloud-platform": { "kind": "badge", "file": null },
|
||||
"ship-courier": { "kind": "ship", "file": "ship-courier.png" },
|
||||
"ship-freighter": { "kind": "ship", "file": "ship-freighter.png" },
|
||||
"ship-bulk-hauler": { "kind": "ship", "file": "ship-bulk-hauler.png" },
|
||||
"ship-company-courier": { "kind": "ship", "file": "ship-company-courier.png" }
|
||||
"bg": {
|
||||
"kind": "background",
|
||||
"file": "bg.png"
|
||||
},
|
||||
"sun": {
|
||||
"kind": "body",
|
||||
"file": "body-sun.png"
|
||||
},
|
||||
"body-earth": {
|
||||
"kind": "body",
|
||||
"file": "body-earth.png"
|
||||
},
|
||||
"body-moon": {
|
||||
"kind": "body",
|
||||
"file": "body-moon.png"
|
||||
},
|
||||
"body-mars": {
|
||||
"kind": "body",
|
||||
"file": "body-mars.png"
|
||||
},
|
||||
"body-ceres": {
|
||||
"kind": "body",
|
||||
"file": "body-ceres.png"
|
||||
},
|
||||
"body-jupiter": {
|
||||
"kind": "body",
|
||||
"file": "body-jupiter.png"
|
||||
},
|
||||
"badge-cloud-platform": {
|
||||
"kind": "badge",
|
||||
"file": null
|
||||
},
|
||||
"ship-courier": {
|
||||
"kind": "ship",
|
||||
"file": "ship-courier.png"
|
||||
},
|
||||
"ship-freighter": {
|
||||
"kind": "ship",
|
||||
"file": "ship-freighter.png"
|
||||
},
|
||||
"ship-bulk-hauler": {
|
||||
"kind": "ship",
|
||||
"file": "ship-bulk-hauler.png"
|
||||
},
|
||||
"ship-company-courier": {
|
||||
"kind": "ship",
|
||||
"file": "ship-company-courier.png"
|
||||
},
|
||||
"ship-lighter": {
|
||||
"kind": "ship",
|
||||
"file": null
|
||||
},
|
||||
"ship-clipper": {
|
||||
"kind": "ship",
|
||||
"file": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ Spec: `memory-bank/change-requests/CR-011-settings-backup.md` (received as "CR-0
|
|||
|
||||
Spec: `memory-bank/change-requests/CR-012-lighter-clipper-range.md` (received as "CR-009 … Phase 10I"). New decisions recorded as plan §0.5.77–0.5.81; §0.5.67 and §0.5.68 amended.
|
||||
|
||||
- [ ] Step 10L.1 — Content: range factors and new ships
|
||||
- [x] **Step 10L.1 — Content: range factors and new ships.** `Location.launchRangeFactor` (Earth 1, Moon 1.3, Mars 1.15, Ceres 1.3, Jupiter 1). `ShipModel.role` (all six) and optional `ShipModel.handlingGameHoursPerTonne` (the sim's effective stats now start from it). New models: **Lighter** (42,400 cr, 1.6M km/h, 80 t, range 10, overhead 2 h, handling 0.012 h/t, fuel 350/M km) and **Clipper** (61,600 cr, 6.5M km/h, 30 t, range 198, overhead 2 h, fuel 600/M km); archetypes `lighter`, `clipper`. Config `noStrandMarginGameHours` 48. Manifest lists `ship-lighter` / `ship-clipper` as pending art. Tests: the CR's orderings replace Step 2.3's (`ships.test.ts`), range factors (`locations.test.ts`), Lighter handling (`effective-stats.test.ts`).
|
||||
- [ ] Step 10L.2 — Sim: effective range from origin
|
||||
- [ ] Step 10L.3 — Sim: no-strand rule
|
||||
- [ ] Step 10L.4 — Content tuning: reach table
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ export const gameConfig: GameConfig = {
|
|||
// Courier price + Earth bay lease.
|
||||
dealLoanAmount: 5000,
|
||||
contractBoardPeriodRealMinutes: 10,
|
||||
// A ship must be able to fly out of its destination two game days after arriving (CR-012).
|
||||
noStrandMarginGameHours: 48,
|
||||
// Ship upgrades (CR-009): level 1, 2, 3.
|
||||
upgradeLevels: [
|
||||
{ costPercentOfPrice: 10, shopRealMinutes: 10 },
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export const locations: Location[] = [
|
|||
body: "earth",
|
||||
sprite: "body-earth",
|
||||
launchCostMultiplier: 3,
|
||||
launchRangeFactor: 1,
|
||||
startsDiscovered: true,
|
||||
bayLeasePrice: 1000,
|
||||
produces: goodsProducedAt("earth"),
|
||||
|
|
@ -37,6 +38,7 @@ export const locations: Location[] = [
|
|||
body: "moon",
|
||||
sprite: "body-moon",
|
||||
launchCostMultiplier: 0.5,
|
||||
launchRangeFactor: 1.3,
|
||||
startsDiscovered: true,
|
||||
bayLeasePrice: 800,
|
||||
produces: goodsProducedAt("moon"),
|
||||
|
|
@ -55,6 +57,7 @@ export const locations: Location[] = [
|
|||
body: "mars",
|
||||
sprite: "body-mars",
|
||||
launchCostMultiplier: 1.5,
|
||||
launchRangeFactor: 1.15,
|
||||
startsDiscovered: false,
|
||||
bayLeasePrice: 20000,
|
||||
produces: goodsProducedAt("mars"),
|
||||
|
|
@ -70,6 +73,7 @@ export const locations: Location[] = [
|
|||
body: "ceres",
|
||||
sprite: "body-ceres",
|
||||
launchCostMultiplier: 0.3,
|
||||
launchRangeFactor: 1.3,
|
||||
startsDiscovered: false,
|
||||
bayLeasePrice: 40000,
|
||||
produces: goodsProducedAt("ceres-station"),
|
||||
|
|
@ -86,6 +90,7 @@ export const locations: Location[] = [
|
|||
sprite: "body-jupiter",
|
||||
badge: "badge-cloud-platform",
|
||||
launchCostMultiplier: 2,
|
||||
launchRangeFactor: 1,
|
||||
startsDiscovered: false,
|
||||
bayLeasePrice: 80000,
|
||||
produces: goodsProducedAt("jupiter-cloud-platform"),
|
||||
|
|
|
|||
|
|
@ -21,6 +21,47 @@ export const shipModels: ShipModel[] = [
|
|||
overheadGameHours: 2,
|
||||
fuelCostPerMillionKm: 200,
|
||||
purchasable: true,
|
||||
role: "Fast and cheap: small loads around Earth and the Moon.",
|
||||
},
|
||||
{
|
||||
id: "lighter",
|
||||
sprite: "ship-lighter",
|
||||
name: "Lighter",
|
||||
archetype: "lighter",
|
||||
// About 40 % of the way from Courier to Freighter.
|
||||
price: 42_400,
|
||||
// Slower than the Freighter, faster than the Bulk Hauler.
|
||||
cruiseSpeedKmPerGameHour: 1_600_000,
|
||||
cargoCapacityTonnes: 80,
|
||||
// Earth and the Moon only, even with maxed tanks (CR-012 §3).
|
||||
fuelRangeMillionKm: 10,
|
||||
hullRating: 2,
|
||||
landingClass: "atmospheric",
|
||||
overheadGameHours: 2,
|
||||
// Big cargo doors: the fastest loading in the fleet.
|
||||
handlingGameHoursPerTonne: 0.012,
|
||||
fuelCostPerMillionKm: 350,
|
||||
purchasable: true,
|
||||
role: "Short-haul heavy lifter: big loads between Earth and the Moon, loaded fast.",
|
||||
},
|
||||
{
|
||||
id: "clipper",
|
||||
sprite: "ship-clipper",
|
||||
name: "Clipper",
|
||||
archetype: "clipper",
|
||||
// About 60 % of the way from Courier to Freighter.
|
||||
price: 61_600,
|
||||
// The fastest ship in the game.
|
||||
cruiseSpeedKmPerGameHour: 6_500_000,
|
||||
cargoCapacityTonnes: 30,
|
||||
// Mars near opposition, Ceres by way of Mars (CR-012 §3; tuned in 10L.4).
|
||||
fuelRangeMillionKm: 198,
|
||||
hullRating: 1,
|
||||
landingClass: "atmospheric",
|
||||
overheadGameHours: 2,
|
||||
fuelCostPerMillionKm: 600,
|
||||
purchasable: true,
|
||||
role: "Fast long-legged runner: the cheapest way to Mars, built for Priority work.",
|
||||
},
|
||||
{
|
||||
id: "freighter",
|
||||
|
|
@ -37,6 +78,7 @@ export const shipModels: ShipModel[] = [
|
|||
overheadGameHours: 4,
|
||||
fuelCostPerMillionKm: 400,
|
||||
purchasable: true,
|
||||
role: "The all-rounder: big loads to Mars and Ceres, Jupiter by way of Ceres.",
|
||||
},
|
||||
{
|
||||
id: "bulk-hauler",
|
||||
|
|
@ -53,6 +95,7 @@ export const shipModels: ShipModel[] = [
|
|||
overheadGameHours: 8,
|
||||
fuelCostPerMillionKm: 800,
|
||||
purchasable: true,
|
||||
role: "Slow and enormous: the only ship that can fly any leg in the system.",
|
||||
},
|
||||
{
|
||||
// The company's tutorial loaner (decision 0.5.29): not for sale. Faster with
|
||||
|
|
@ -70,5 +113,6 @@ export const shipModels: ShipModel[] = [
|
|||
overheadGameHours: 0.3,
|
||||
fuelCostPerMillionKm: 200,
|
||||
purchasable: false,
|
||||
role: "Halvorsen's loaner for the company runs.",
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@ export const locationSchema = z.object({
|
|||
body: astronomyBodySchema,
|
||||
/** Multiplies fuel cost for launches from this location. */
|
||||
launchCostMultiplier: z.number().positive(),
|
||||
/**
|
||||
* Multiplies a ship's range for legs launched from here (decision 0.5.78):
|
||||
* low gravity stretches it. Separate from the fuel-cost multiplier.
|
||||
*/
|
||||
launchRangeFactor: z.number().positive(),
|
||||
startsDiscovered: z.boolean(),
|
||||
/** Credits. */
|
||||
bayLeasePrice: nonNegativeInt,
|
||||
|
|
@ -59,7 +64,13 @@ export const locationSchema = z.object({
|
|||
});
|
||||
export type Location = z.infer<typeof locationSchema>;
|
||||
|
||||
export const shipArchetypeSchema = z.enum(["courier", "freighter", "bulk-hauler"]);
|
||||
export const shipArchetypeSchema = z.enum([
|
||||
"courier",
|
||||
"lighter",
|
||||
"clipper",
|
||||
"freighter",
|
||||
"bulk-hauler",
|
||||
]);
|
||||
export type ShipArchetype = z.infer<typeof shipArchetypeSchema>;
|
||||
|
||||
export const shipModelSchema = z.object({
|
||||
|
|
@ -80,6 +91,10 @@ export const shipModelSchema = z.object({
|
|||
fuelCostPerMillionKm: z.number().nonnegative(),
|
||||
/** False for company-only ships such as the tutorial loaner (decision 0.5.29). */
|
||||
purchasable: z.boolean(),
|
||||
/** One line on what the ship is for, shown in the Market (CR-012). */
|
||||
role: z.string().min(1),
|
||||
/** Loading time per tonne; defaults to the game-wide `handlingGameHoursPerTonne` (CR-012). */
|
||||
handlingGameHoursPerTonne: z.number().positive().optional(),
|
||||
/** Map art asset id (CR-003), e.g. `ship-freighter`. */
|
||||
sprite: idSchema,
|
||||
});
|
||||
|
|
@ -198,6 +213,8 @@ export const gameConfigSchema = z
|
|||
/** Loan taken on with the deal, in credits (decision 0.5.30). */
|
||||
dealLoanAmount: positiveInt,
|
||||
contractBoardPeriodRealMinutes: z.number().positive(),
|
||||
/** No-strand check: the ship must have a way out this long after arrival (decision 0.5.79). */
|
||||
noStrandMarginGameHours: z.number().nonnegative(),
|
||||
/**
|
||||
* Upgrade levels 1, 2, 3, …: cost as a percent of the ship model's price and
|
||||
* real minutes in the shop (CR-009).
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import { content } from "../src/index";
|
|||
describe("content bundle", () => {
|
||||
it("contains every kind of content", () => {
|
||||
expect(content.locations).toHaveLength(5);
|
||||
expect(content.shipModels).toHaveLength(4);
|
||||
expect(content.purchasableShipModels).toHaveLength(3);
|
||||
expect(content.shipModels).toHaveLength(6);
|
||||
expect(content.purchasableShipModels).toHaveLength(5);
|
||||
expect(content.cargoTypes).toHaveLength(4);
|
||||
expect(content.skills).toHaveLength(4);
|
||||
expect(content.config.timeCompression).toBe(60);
|
||||
|
|
|
|||
|
|
@ -49,4 +49,16 @@ describe("locations", () => {
|
|||
for (const sprite of sprites) expect(sprite).toMatch(/^[a-z][a-z-]*$/);
|
||||
expect(new Set(sprites).size).toBe(sprites.length);
|
||||
});
|
||||
|
||||
it("gives every location a positive launch range factor (CR-012)", () => {
|
||||
for (const l of locations) expect(l.launchRangeFactor, l.id).toBeGreaterThan(0);
|
||||
const factor = (id: string) => locations.find((l) => l.id === id)!.launchRangeFactor;
|
||||
expect([
|
||||
factor("earth"),
|
||||
factor("moon"),
|
||||
factor("mars"),
|
||||
factor("ceres-station"),
|
||||
factor("jupiter-cloud-platform"),
|
||||
]).toEqual([1, 1.3, 1.15, 1.3, 1]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ describe("loaner ship", () => {
|
|||
it("is not in the Market's purchasable list", () => {
|
||||
const ids = content.purchasableShipModels.map((s) => s.id);
|
||||
expect(ids).not.toContain("company-courier");
|
||||
expect(ids).toEqual(["courier", "freighter", "bulk-hauler"]);
|
||||
expect(ids).toEqual(["courier", "lighter", "clipper", "freighter", "bulk-hauler"]);
|
||||
});
|
||||
|
||||
it("is faster with less overhead than the Courier", () => {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const validLocation = {
|
|||
landingRequirement: "atmospheric",
|
||||
body: "earth",
|
||||
launchCostMultiplier: 3,
|
||||
launchRangeFactor: 1,
|
||||
startsDiscovered: true,
|
||||
bayLeasePrice: 5000,
|
||||
sprite: "body-mars",
|
||||
|
|
@ -52,6 +53,7 @@ const validShipModel = {
|
|||
overheadGameHours: 2,
|
||||
fuelCostPerMillionKm: 100,
|
||||
purchasable: true,
|
||||
role: "Fast and cheap.",
|
||||
sprite: "ship-courier",
|
||||
};
|
||||
|
||||
|
|
@ -78,6 +80,7 @@ const validGameConfig = {
|
|||
contractBoardSize: 8,
|
||||
tutorialBoardSize: 50,
|
||||
contractBoardPeriodRealMinutes: 60,
|
||||
noStrandMarginGameHours: 48,
|
||||
dutyLimitGameHours: 12,
|
||||
longHaulDutyBonusGameHours: 2,
|
||||
restGameHours: 60,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { shipModelSchema, shipModels } from "../src/index";
|
||||
import { shipModelSchema, shipModels, gameConfig } from "../src/index";
|
||||
|
||||
const ship = (id: string) => shipModels.find((s) => s.id === id)!;
|
||||
|
||||
|
|
@ -10,39 +10,67 @@ describe("ship models", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("has the three archetypes, all atmospheric", () => {
|
||||
// CR-012 §2 replaces the Step 2.3 ordering tests.
|
||||
const order = (
|
||||
key: "price" | "cruiseSpeedKmPerGameHour" | "cargoCapacityTonnes",
|
||||
ids: string[],
|
||||
) =>
|
||||
ids
|
||||
.slice(1)
|
||||
.forEach((id, i) =>
|
||||
expect(ship(id)[key], `${ids[i]} < ${id}`).toBeGreaterThan(ship(ids[i]!)[key]),
|
||||
);
|
||||
const purchasable = shipModels.filter((m) => m.purchasable);
|
||||
const handling = (id: string) =>
|
||||
ship(id).handlingGameHoursPerTonne ?? gameConfig.handlingGameHoursPerTonne;
|
||||
|
||||
it("has five purchasable models plus the loaner, all atmospheric", () => {
|
||||
expect(shipModels.map((s) => s.id)).toEqual([
|
||||
"courier",
|
||||
"lighter",
|
||||
"clipper",
|
||||
"freighter",
|
||||
"bulk-hauler",
|
||||
"company-courier",
|
||||
]);
|
||||
for (const model of shipModels) {
|
||||
expect(model.landingClass).toBe("atmospheric");
|
||||
expect(model.role.length).toBeGreaterThan(10);
|
||||
}
|
||||
expect(ship("lighter").purchasable && ship("clipper").purchasable).toBe(true);
|
||||
});
|
||||
|
||||
it("orders speed Courier > Freighter > Bulk Hauler", () => {
|
||||
expect(ship("courier").cruiseSpeedKmPerGameHour).toBeGreaterThan(
|
||||
ship("freighter").cruiseSpeedKmPerGameHour,
|
||||
);
|
||||
expect(ship("freighter").cruiseSpeedKmPerGameHour).toBeGreaterThan(
|
||||
ship("bulk-hauler").cruiseSpeedKmPerGameHour,
|
||||
);
|
||||
it("orders price Courier < Lighter < Clipper < Freighter < Bulk Hauler", () => {
|
||||
order("price", ["courier", "lighter", "clipper", "freighter", "bulk-hauler"]);
|
||||
});
|
||||
|
||||
it("orders capacity Bulk Hauler > Freighter > Courier", () => {
|
||||
expect(ship("bulk-hauler").cargoCapacityTonnes).toBeGreaterThan(
|
||||
ship("freighter").cargoCapacityTonnes,
|
||||
);
|
||||
expect(ship("freighter").cargoCapacityTonnes).toBeGreaterThan(
|
||||
ship("courier").cargoCapacityTonnes,
|
||||
it("makes the Clipper fastest, the Bulk Hauler slowest, and the Lighter slower than the Freighter", () => {
|
||||
const speeds = shipModels.map((m) => m.cruiseSpeedKmPerGameHour);
|
||||
expect(ship("clipper").cruiseSpeedKmPerGameHour).toBe(Math.max(...speeds));
|
||||
expect(ship("bulk-hauler").cruiseSpeedKmPerGameHour).toBe(
|
||||
Math.min(...purchasable.map((m) => m.cruiseSpeedKmPerGameHour)),
|
||||
);
|
||||
order("cruiseSpeedKmPerGameHour", ["bulk-hauler", "lighter", "freighter"]);
|
||||
});
|
||||
|
||||
it("orders price Courier < Freighter < Bulk Hauler", () => {
|
||||
expect(ship("courier").price).toBeLessThan(ship("freighter").price);
|
||||
expect(ship("freighter").price).toBeLessThan(ship("bulk-hauler").price);
|
||||
it("orders capacity Courier < Clipper < Lighter < Freighter < Bulk Hauler", () => {
|
||||
order("cargoCapacityTonnes", ["courier", "clipper", "lighter", "freighter", "bulk-hauler"]);
|
||||
const lighterShare =
|
||||
ship("lighter").cargoCapacityTonnes / ship("freighter").cargoCapacityTonnes;
|
||||
expect(lighterShare).toBeGreaterThanOrEqual(0.75);
|
||||
expect(lighterShare).toBeLessThanOrEqual(0.9);
|
||||
});
|
||||
|
||||
it("gives the Lighter the fastest loading and the Clipper costly fuel", () => {
|
||||
for (const m of shipModels) {
|
||||
if (m.id !== "lighter") expect(handling("lighter")).toBeLessThan(handling(m.id));
|
||||
}
|
||||
expect(ship("clipper").fuelCostPerMillionKm).toBeGreaterThan(
|
||||
ship("courier").fuelCostPerMillionKm,
|
||||
);
|
||||
expect(ship("clipper").fuelCostPerMillionKm).toBeGreaterThan(
|
||||
ship("freighter").fuelCostPerMillionKm,
|
||||
);
|
||||
});
|
||||
|
||||
it("gives every ship model its own map sprite key (CR-003)", () => {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
const model = (id: string) => shipModels.find((m) => m.id === id)!;
|
||||
const upgradeFor = (stat: ShipStat) => upgrades.find((u) => u.stat === stat)!;
|
||||
/** A stat at the upgrade's max level for this model (decision 0.5.63: linear % of base). */
|
||||
const maxed = (modelId: string, stat: ShipStat & keyof (typeof shipModels)[number]) => {
|
||||
const maxed = (modelId: string, stat: Exclude<ShipStat, "handlingGameHoursPerTonne">) => {
|
||||
const upgrade = upgradeFor(stat);
|
||||
const level = upgradeMaxLevel(upgrade, modelId);
|
||||
return model(modelId)[stat] * (1 + (upgradePercentPerLevel(upgrade, modelId) * level) / 100);
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@ export function effectiveStats(
|
|||
): EffectiveStats {
|
||||
const stats: EffectiveStats = {
|
||||
...model,
|
||||
handlingGameHoursPerTonne: content.config.handlingGameHoursPerTonne,
|
||||
// The model's own loading time if it has one (the Lighter, CR-012), else the game-wide base.
|
||||
handlingGameHoursPerTonne:
|
||||
model.handlingGameHoursPerTonne ?? content.config.handlingGameHoursPerTonne,
|
||||
};
|
||||
for (const upgrade of content.upgrades) {
|
||||
const level = Math.min(levels[upgrade.id] ?? 0, upgradeMaxLevel(upgrade, model.id));
|
||||
|
|
|
|||
|
|
@ -116,3 +116,19 @@ describe("effective stats (Step 10I.2)", () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("per-model handling (CR-012)", () => {
|
||||
it("loads the Lighter faster than other ships, and upgrades scale its own base", () => {
|
||||
const lighter = content.shipModel("lighter")!;
|
||||
expect(effectiveStats(lighter, {}, content).handlingGameHoursPerTonne).toBe(
|
||||
lighter.handlingGameHoursPerTonne,
|
||||
);
|
||||
expect(
|
||||
effectiveStats(lighter, { "handling-gear": 1 }, content).handlingGameHoursPerTonne,
|
||||
).toBeCloseTo(lighter.handlingGameHoursPerTonne! * 0.85, 9);
|
||||
expect(travelTimeGameHours(lighter, 0, 50, content.config)).toBeCloseTo(
|
||||
lighter.overheadGameHours + 50 * lighter.handlingGameHoursPerTonne!,
|
||||
9,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue