diff --git a/DECISIONS.md b/DECISIONS.md index 4d80ee3..c21e0c1 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -994,3 +994,9 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **Renumbered on arrival:** titled "CR-007 … Phase 10G"; CR-007 to CR-009 and Phases 10G–10I already exist, so it is **CR-010**, done as **Phase 10J** (its 10G.n are 10J.n). - **Decisions:** 0.5.12 Pilot pay is replaced in place (profit share). The CR's 0.5.51 and 0.5.52 are **0.5.69 Deduction order** and **0.5.70 Veteran pilot**. Its "CR-001 decision 0.5.24" (the loan) is this plan's §0.5.32. - **Goal and timing target changes** to CR-001's goal 4 and the "hire first pilot" target are applied in their steps (10J.3, 10J.7). + +## Step 10J.1 + +- **The flat fee is removed one step later**, in 10J.2 together with the formula that replaces it, so every commit builds. `pilotProfitShare` is added here. +- **The veteran lives with the onboarding content** (`content.veteranPilot`: name, bio, level, skills) since it belongs to the deal. Name and bio are from the CR's example. +- **Skill points are checked against the level:** a level-2 pilot has 2 × 2 = 4 points; the veteran spends 1 (Heavy Cargo 1) and has 3 unspent. diff --git a/memory-bank/progress.md b/memory-bank/progress.md index bb8da41..18d13fd 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -240,7 +240,7 @@ Spec: `memory-bank/change-requests/CR-009-ship-upgrades.md` (received as "CR-006 Spec: `memory-bank/change-requests/CR-010-veteran-pilot-profit-share.md` (received as "CR-007 … Phase 10G"). Decision 0.5.12 replaced; new decisions recorded as plan §0.5.69–0.5.70. -- [ ] Step 10J.1 — Content: config and veteran +- [x] **Step 10J.1 — Content: config and veteran.** Config `pilotProfitShare` 0.25 (a fraction 0–1); the flat `pilotPayPerDelivery` is removed in 10J.2 when the sim switches to the share. `veteranPilotSchema` and `content.veteranPilot`: Rosa Kell, "Halvorsen's pilot for twelve years.", level 2, Heavy Cargo 1. Test: `content/test/veteran.test.ts`. - [ ] Step 10J.2 — Sim: profit-share pay - [ ] Step 10J.3 — Sim: goal change - [ ] Step 10J.4 — Server: veteran on the deal diff --git a/packages/content/src/bundle.ts b/packages/content/src/bundle.ts index 68670a3..635116b 100644 --- a/packages/content/src/bundle.ts +++ b/packages/content/src/bundle.ts @@ -2,7 +2,7 @@ import { cargoTypes, skills } from "./data/cargo"; import { gameConfig } from "./data/config"; import { goods } from "./data/goods"; import { locations } from "./data/locations"; -import { onboardingGoals, scriptedTutorialContracts } from "./data/onboarding"; +import { onboardingGoals, scriptedTutorialContracts, veteranPilot } from "./data/onboarding"; import { shipModels } from "./data/ships"; import { upgrades } from "./data/upgrades"; @@ -24,6 +24,7 @@ export const content = { config: gameConfig, scriptedTutorialContracts, onboardingGoals, + veteranPilot, upgrades, location: byId(locations), shipModel: byId(shipModels), diff --git a/packages/content/src/data/config.ts b/packages/content/src/data/config.ts index a1a1e65..eac31d7 100644 --- a/packages/content/src/data/config.ts +++ b/packages/content/src/data/config.ts @@ -65,4 +65,6 @@ export const gameConfig: GameConfig = { skillPointsPerLevel: 2, pilotHireCost: 2000, pilotPayPerDelivery: 100, + // A quarter of each contract's profit (payout − fuel) goes to a hired pilot (CR-010). + pilotProfitShare: 0.25, }; diff --git a/packages/content/src/data/onboarding.ts b/packages/content/src/data/onboarding.ts index 17efc2b..cbde6c0 100644 --- a/packages/content/src/data/onboarding.ts +++ b/packages/content/src/data/onboarding.ts @@ -1,4 +1,4 @@ -import type { OnboardingGoal, ScriptedTutorialContract } from "../schemas"; +import type { OnboardingGoal, ScriptedTutorialContract, VeteranPilot } from "../schemas"; /** Special tutorial contracts by run (decision 0.5.33). */ export const scriptedTutorialContracts: ScriptedTutorialContract[] = [ @@ -52,3 +52,14 @@ export const onboardingGoals: OnboardingGoal[] = [ reward: 2500, }, ]; + +/** + * Halvorsen's last hire, who comes with the deal (decision 0.5.70): level 2, + * one point already in Heavy Cargo, the rest unspent. + */ +export const veteranPilot: VeteranPilot = { + name: "Rosa Kell", + bio: "Halvorsen's pilot for twelve years.", + level: 2, + skills: { "heavy-cargo": 1 }, +}; diff --git a/packages/content/src/schemas.ts b/packages/content/src/schemas.ts index b3784c0..c91c33a 100644 --- a/packages/content/src/schemas.ts +++ b/packages/content/src/schemas.ts @@ -265,6 +265,8 @@ export const gameConfigSchema = z pilotHireCost: nonNegativeInt, /** Credits per delivery; the avatar is exempt (decision 0.5.12). */ pilotPayPerDelivery: nonNegativeInt, + /** Hired pilots' share of profit (payout − fuel) per contract delivery (decision 0.5.12, CR-010). */ + pilotProfitShare: fraction, }) .refine( (config) => @@ -315,3 +317,14 @@ export const onboardingGoalSchema = z.object({ reward: positiveInt, }); export type OnboardingGoal = z.infer; + +/** The veteran pilot who comes with the deal (decision 0.5.70, CR-010). */ +export const veteranPilotSchema = z.object({ + name: z.string().min(1), + /** One line shown on the deal and the pilot card. */ + bio: z.string().min(1), + level: positiveInt, + /** Skill levels already bought, by skill id. */ + skills: z.record(idSchema, positiveInt), +}); +export type VeteranPilot = z.infer; diff --git a/packages/content/test/schemas.test.ts b/packages/content/test/schemas.test.ts index 4819936..dd221d2 100644 --- a/packages/content/test/schemas.test.ts +++ b/packages/content/test/schemas.test.ts @@ -100,6 +100,7 @@ const validGameConfig = { skillPointsPerLevel: 1, pilotHireCost: 1000, pilotPayPerDelivery: 100, + pilotProfitShare: 0.25, tutorialLoanerShipModelId: "company-courier", tutorialRunsBeforeDeal: 4, tutorialMassTonnes: { light: 5, medium: 12, full: 20 }, diff --git a/packages/content/test/veteran.test.ts b/packages/content/test/veteran.test.ts new file mode 100644 index 0000000..05ffa11 --- /dev/null +++ b/packages/content/test/veteran.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import { + content, + gameConfig, + gameConfigSchema, + veteranPilot, + veteranPilotSchema, +} from "../src/index"; + +describe("veteran pilot and profit share (CR-010)", () => { + it("has a valid profit share between 0 and 1", () => { + expect(gameConfigSchema.safeParse(gameConfig).success).toBe(true); + expect(gameConfig.pilotProfitShare).toBeGreaterThan(0); + expect(gameConfig.pilotProfitShare).toBeLessThan(1); + expect(gameConfigSchema.safeParse({ ...gameConfig, pilotProfitShare: 1.2 }).success).toBe( + false, + ); + }); + + it("gives the veteran a valid level and existing skills within their skill points", () => { + expect(veteranPilotSchema.safeParse(veteranPilot).success).toBe(true); + expect(veteranPilot.level).toBe(2); + expect(veteranPilot.level).toBeLessThanOrEqual(gameConfig.pilotLevelXpThresholds.length); + const spent = Object.values(veteranPilot.skills).reduce((a, b) => a + b, 0); + expect(spent).toBeLessThanOrEqual(veteranPilot.level * gameConfig.skillPointsPerLevel); + for (const [skillId, level] of Object.entries(veteranPilot.skills)) { + const skill = content.skill(skillId); + expect(skill, skillId).toBeDefined(); + expect(level).toBeLessThanOrEqual(skill!.maxLevel); + } + expect(veteranPilot.skills).toEqual({ "heavy-cargo": 1 }); + }); +});