diff --git a/DECISIONS.md b/DECISIONS.md index db0e2fe..e79c4ee 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1554,3 +1554,7 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` ## CR-017 recorded (Tutorial XP and Skill Point Prompt) - **Numbering:** received as "CR-016 — Tutorial XP and Skill Point Prompt" for "Phase 16"; stored as **CR-017**, built as **Phase 17** (its 16.n = 17.n). Its decisions CR-016.D1–D4 are **0.5.106–0.5.109**. Its references: CR-007 → CR-010, CR-008 → CR-011, CR-010 → CR-013, CR-011 → CR-014; its "CR-001 0.5.22" is 0.5.30. The numbering note at the top of the CR file has the full map. + +## Step 17.1 + +- **The "loaner at Earth" condition went with the run count** (it is Step 17.4's rule): with five runs ending at the Moon, the deal could never come otherwise. No placement code was needed: accepting the deal assigns the avatar to the Courier at the Earth bay, and an assigned pilot is wherever their ship is (decision 0.5.27). diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 6265f22..7cdec22 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -414,7 +414,7 @@ Spec: `memory-bank/change-requests/CR-016-venus-io-europa-belt.md` (received as Spec: `memory-bank/change-requests/CR-017-tutorial-xp-skill-prompt.md` (received as "CR-016 … Phase 16"). New decisions recorded as plan §0.5.106–0.5.109. -- [ ] Step 17.1 — Content: tutorial settings +- [x] **Step 17.1 — Content: tutorial settings.** GameConfig: `tutorialRunsBeforeDeal` 5 (was 4) and a new `tutorialXpMultiplier` 2 (at least 1; used from 17.2). Pulled forward so the game keeps working: the deal no longer waits for the loaner to be back at Earth (decision 0.5.108) in the engine (`isDealAvailable`) and the balance simulator, since five runs end at the Moon; accepting it already puts the avatar on the Courier at the Earth bay. Balance medians: deal 3.7 min, first purchase 92.6 (45–95), discovery 49.6, loan 138.5, four ships 136 vs 138. Tests: `packages/content/test/config.test.ts` (five runs at double XP; a multiplier under 1 is rejected); the engine's deal tests now expect the deal after five runs ending at the Moon (not after four), after three with the three-run config, and accepting with the loaner at the Moon to put the avatar and the Courier at Earth. - [ ] Step 17.2 — Sim: tutorial XP and the level-1 guarantee - [ ] Step 17.3 — Sim: tutorial timing - [ ] Step 17.4 — Engine: deal availability and placement diff --git a/packages/content/src/data/config.ts b/packages/content/src/data/config.ts index 21fa8a6..1008603 100644 --- a/packages/content/src/data/config.ts +++ b/packages/content/src/data/config.ts @@ -16,7 +16,9 @@ export const gameConfig: GameConfig = { // One tutorial contract per mass tier (CR-001). tutorialBoardSize: 3, tutorialLoanerShipModelId: "company-courier", - tutorialRunsBeforeDeal: 4, + // Five runs, at double XP, so the avatar has a skill point at the deal (CR-017). + tutorialRunsBeforeDeal: 5, + tutorialXpMultiplier: 2, tutorialMassTonnes: { light: 5, medium: 12, full: 20 }, // 20 t adds 0.4 game hours (24 real seconds) of loading and unloading. handlingGameHoursPerTonne: 0.02, diff --git a/packages/content/src/schemas.ts b/packages/content/src/schemas.ts index ab72865..8a199de 100644 --- a/packages/content/src/schemas.ts +++ b/packages/content/src/schemas.ts @@ -216,8 +216,10 @@ export const gameConfigSchema = z tutorialBoardSize: positiveInt, /** Ship model the company lends during the tutorial (decision 0.5.29). */ tutorialLoanerShipModelId: idSchema, - /** Tutorial runs completed before the deal is offered (decision 0.5.30). */ + /** Tutorial runs completed before the deal is offered (decisions 0.5.30, 0.5.106). */ tutorialRunsBeforeDeal: positiveInt, + /** Tutorial runs earn normal XP × this (decision 0.5.107). */ + tutorialXpMultiplier: z.number().min(1), /** Cargo mass of the three tutorial contracts (decision 0.5.31). */ tutorialMassTonnes: z .object({ diff --git a/packages/content/test/config.test.ts b/packages/content/test/config.test.ts index 404d515..6c73ad4 100644 --- a/packages/content/test/config.test.ts +++ b/packages/content/test/config.test.ts @@ -84,3 +84,17 @@ describe("game config", () => { expect(gameConfig.longHaulDutyBonusGameHours).toBeGreaterThan(0); }); }); + +describe("tutorial settings (Step 17.1, decisions 0.5.106, 0.5.107)", () => { + it("runs five tutorial runs before the deal, at double XP", () => { + expect(gameConfig.tutorialRunsBeforeDeal).toBe(5); + expect(gameConfig.tutorialXpMultiplier).toBe(2); + }); + + it("never lets the tutorial XP multiplier fall below 1", () => { + expect(gameConfigSchema.safeParse(gameConfig).success).toBe(true); + expect(gameConfigSchema.safeParse({ ...gameConfig, tutorialXpMultiplier: 0.5 }).success).toBe( + false, + ); + }); +}); diff --git a/packages/content/test/schemas.test.ts b/packages/content/test/schemas.test.ts index 0dd33cf..ddc295f 100644 --- a/packages/content/test/schemas.test.ts +++ b/packages/content/test/schemas.test.ts @@ -104,7 +104,8 @@ const validGameConfig = { pilotHireCost: 1000, pilotProfitShare: 0.25, tutorialLoanerShipModelId: "company-courier", - tutorialRunsBeforeDeal: 4, + tutorialRunsBeforeDeal: 5, + tutorialXpMultiplier: 2, tutorialMassTonnes: { light: 5, medium: 12, full: 20 }, handlingGameHoursPerTonne: 0.02, upgradeLevels: [{ costPercentOfPrice: 10, shopRealMinutes: 10 }], diff --git a/packages/engine/src/game/state.ts b/packages/engine/src/game/state.ts index 4fed5fc..bc5b850 100644 --- a/packages/engine/src/game/state.ts +++ b/packages/engine/src/game/state.ts @@ -190,7 +190,7 @@ export function isDealAvailable(save: SaveDocument): boolean { player.tutorialActive && !player.dealAccepted && player.tutorialRunsCompleted >= content.config.tutorialRunsBeforeDeal && - player.loanerLocationId === "earth" && + // Wherever the loaner is (decision 0.5.108): accepting puts the avatar at Earth. !runInFlight ); } diff --git a/packages/engine/test/ported/deal.test.ts b/packages/engine/test/ported/deal.test.ts index 4cdf22a..eaff180 100644 --- a/packages/engine/test/ported/deal.test.ts +++ b/packages/engine/test/ported/deal.test.ts @@ -81,13 +81,20 @@ describe("accepting the deal", () => { expect(await snapshot(guest)).toEqual(before); }); - it("fails while the loaner is at the Moon", async () => { + it("is accepted with the loaner at the Moon, and the avatar ends up at Earth (CR-017)", async () => { const guest = await signUp(testApp().app); await readyForDeal(guest); await edit(testApp().app, (save) => { save.player.loanerLocationId = "moon"; }); - expect((await accept(guest)).statusCode).toBe(409); + const response = await accept(guest); + expect(response.statusCode).toBe(200); + const state = playerStateSchema.parse(response.json()); + const avatar = state.pilots.find((p) => p.isAvatar)!; + const courier = state.ships.find((s) => s.id === avatar.assignedShipId)!; + expect(avatar.locationId).toBe("earth"); + expect(courier).toMatchObject({ modelId: "courier", locationId: "earth" }); + expect(state.tutorial).toBeNull(); }); it("fails the second time", async () => { diff --git a/packages/engine/test/ported/tutorial-v2-three-runs.test.ts b/packages/engine/test/ported/tutorial-v2-three-runs.test.ts index 6fdd874..22e97ff 100644 --- a/packages/engine/test/ported/tutorial-v2-three-runs.test.ts +++ b/packages/engine/test/ported/tutorial-v2-three-runs.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from "vitest"; import { useTestApp, signUp, getState, flyTutorialRun } from "../support"; -// The deal after three runs instead of four: the loaner is then at the Moon. +// The deal after three runs instead of five: the loaner is then at the Moon. vi.mock("@deadhead/content", async (importOriginal) => { const original = await importOriginal(); return { @@ -17,19 +17,18 @@ vi.mock("@deadhead/content", async (importOriginal) => { const testApp = useTestApp(); describe("deal availability with tutorialRunsBeforeDeal = 3", () => { - it("waits until a fourth run returns the loaner to Earth", async () => { + it("offers the deal after the third run, though the loaner is at the Moon (decision 0.5.108)", async () => { const guest = await signUp(testApp().app); - for (let run = 1; run <= 3; run++) await flyTutorialRun(testApp(), guest); + for (let run = 1; run <= 2; run++) await flyTutorialRun(testApp(), guest); expect((await getState(testApp().app, guest)).tutorial).toMatchObject({ - runsCompleted: 3, - loanerLocationId: "moon", + runsCompleted: 2, dealAvailable: false, }); const state = await flyTutorialRun(testApp(), guest); expect(state.tutorial).toMatchObject({ - runsCompleted: 4, - loanerLocationId: "earth", + runsCompleted: 3, + loanerLocationId: "moon", dealAvailable: true, }); }); diff --git a/packages/engine/test/ported/tutorial-v2.test.ts b/packages/engine/test/ported/tutorial-v2.test.ts index 2f3a692..9b6617e 100644 --- a/packages/engine/test/ported/tutorial-v2.test.ts +++ b/packages/engine/test/ported/tutorial-v2.test.ts @@ -55,20 +55,20 @@ describe("tutorial flow v2", () => { expect(third.find((c) => c.scriptedId === "unmarked-crates")).toBeDefined(); }); - it("makes the deal available after four runs ending at Earth, not after three", async () => { + it("makes the deal available after five runs ending at the Moon, not after four (CR-017)", async () => { const guest = await signUp(testApp().app); - for (let run = 1; run <= 3; run++) await flyTutorialRun(testApp(), guest); - const afterThree = await getState(testApp().app, guest); - expect(afterThree.tutorial).toMatchObject({ - runsCompleted: 3, - loanerLocationId: "moon", - dealAvailable: false, - }); - - const afterFour = await flyTutorialRun(testApp(), guest); + for (let run = 1; run <= 4; run++) await flyTutorialRun(testApp(), guest); + const afterFour = await getState(testApp().app, guest); expect(afterFour.tutorial).toMatchObject({ runsCompleted: 4, loanerLocationId: "earth", + dealAvailable: false, + }); + + const afterFive = await flyTutorialRun(testApp(), guest); + expect(afterFive.tutorial).toMatchObject({ + runsCompleted: 5, + loanerLocationId: "moon", runInFlight: false, dealAvailable: true, }); diff --git a/packages/sim/balance/simulate.ts b/packages/sim/balance/simulate.ts index 02e25b6..bae1c9d 100644 --- a/packages/sim/balance/simulate.ts +++ b/packages/sim/balance/simulate.ts @@ -384,7 +384,8 @@ export function simulate(options: SimulationOptions): SimulationResult { // --- Tutorial and the deal (server dispatch.ts, deal.ts). --- function playTutorial(): boolean { if (flights.some((f) => f.record.tutorial)) return false; - if (runsCompleted >= config.tutorialRunsBeforeDeal && loanerLocationId === "earth") { + // Wherever the loaner is (decision 0.5.108). + if (runsCompleted >= config.tutorialRunsBeforeDeal) { acceptDeal(); return true; }