Step 10J.1: pilot profit share config and the veteran pilot

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-10-04 21:22:21 -04:00
parent 727b0c02da
commit e0742fff76
8 changed files with 70 additions and 3 deletions

View File

@ -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.

View File

@ -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

View File

@ -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),

View File

@ -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,
};

View File

@ -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 },
};

View File

@ -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<typeof onboardingGoalSchema>;
/** 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<typeof veteranPilotSchema>;

View File

@ -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 },

View File

@ -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 });
});
});