Step 17.1: content: tutorial settings
CI / check (push) Has been cancelled
Details
CI / check (push) Has been cancelled
Details
Five tutorial runs before the deal and a tutorial XP multiplier of 2. The deal no longer waits for the loaner to be back at Earth, since five runs end at the Moon; accepting it still puts the avatar at Earth. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a95b94d73d
commit
35b12e50da
|
|
@ -1554,3 +1554,7 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
||||||
## CR-017 recorded (Tutorial XP and Skill Point Prompt)
|
## 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.
|
- **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).
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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.2 — Sim: tutorial XP and the level-1 guarantee
|
||||||
- [ ] Step 17.3 — Sim: tutorial timing
|
- [ ] Step 17.3 — Sim: tutorial timing
|
||||||
- [ ] Step 17.4 — Engine: deal availability and placement
|
- [ ] Step 17.4 — Engine: deal availability and placement
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@ export const gameConfig: GameConfig = {
|
||||||
// One tutorial contract per mass tier (CR-001).
|
// One tutorial contract per mass tier (CR-001).
|
||||||
tutorialBoardSize: 3,
|
tutorialBoardSize: 3,
|
||||||
tutorialLoanerShipModelId: "company-courier",
|
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 },
|
tutorialMassTonnes: { light: 5, medium: 12, full: 20 },
|
||||||
// 20 t adds 0.4 game hours (24 real seconds) of loading and unloading.
|
// 20 t adds 0.4 game hours (24 real seconds) of loading and unloading.
|
||||||
handlingGameHoursPerTonne: 0.02,
|
handlingGameHoursPerTonne: 0.02,
|
||||||
|
|
|
||||||
|
|
@ -216,8 +216,10 @@ export const gameConfigSchema = z
|
||||||
tutorialBoardSize: positiveInt,
|
tutorialBoardSize: positiveInt,
|
||||||
/** Ship model the company lends during the tutorial (decision 0.5.29). */
|
/** Ship model the company lends during the tutorial (decision 0.5.29). */
|
||||||
tutorialLoanerShipModelId: idSchema,
|
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,
|
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). */
|
/** Cargo mass of the three tutorial contracts (decision 0.5.31). */
|
||||||
tutorialMassTonnes: z
|
tutorialMassTonnes: z
|
||||||
.object({
|
.object({
|
||||||
|
|
|
||||||
|
|
@ -84,3 +84,17 @@ describe("game config", () => {
|
||||||
expect(gameConfig.longHaulDutyBonusGameHours).toBeGreaterThan(0);
|
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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,8 @@ const validGameConfig = {
|
||||||
pilotHireCost: 1000,
|
pilotHireCost: 1000,
|
||||||
pilotProfitShare: 0.25,
|
pilotProfitShare: 0.25,
|
||||||
tutorialLoanerShipModelId: "company-courier",
|
tutorialLoanerShipModelId: "company-courier",
|
||||||
tutorialRunsBeforeDeal: 4,
|
tutorialRunsBeforeDeal: 5,
|
||||||
|
tutorialXpMultiplier: 2,
|
||||||
tutorialMassTonnes: { light: 5, medium: 12, full: 20 },
|
tutorialMassTonnes: { light: 5, medium: 12, full: 20 },
|
||||||
handlingGameHoursPerTonne: 0.02,
|
handlingGameHoursPerTonne: 0.02,
|
||||||
upgradeLevels: [{ costPercentOfPrice: 10, shopRealMinutes: 10 }],
|
upgradeLevels: [{ costPercentOfPrice: 10, shopRealMinutes: 10 }],
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,7 @@ export function isDealAvailable(save: SaveDocument): boolean {
|
||||||
player.tutorialActive &&
|
player.tutorialActive &&
|
||||||
!player.dealAccepted &&
|
!player.dealAccepted &&
|
||||||
player.tutorialRunsCompleted >= content.config.tutorialRunsBeforeDeal &&
|
player.tutorialRunsCompleted >= content.config.tutorialRunsBeforeDeal &&
|
||||||
player.loanerLocationId === "earth" &&
|
// Wherever the loaner is (decision 0.5.108): accepting puts the avatar at Earth.
|
||||||
!runInFlight
|
!runInFlight
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,13 +81,20 @@ describe("accepting the deal", () => {
|
||||||
expect(await snapshot(guest)).toEqual(before);
|
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);
|
const guest = await signUp(testApp().app);
|
||||||
await readyForDeal(guest);
|
await readyForDeal(guest);
|
||||||
await edit(testApp().app, (save) => {
|
await edit(testApp().app, (save) => {
|
||||||
save.player.loanerLocationId = "moon";
|
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 () => {
|
it("fails the second time", async () => {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { useTestApp, signUp, getState, flyTutorialRun } from "../support";
|
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) => {
|
vi.mock("@deadhead/content", async (importOriginal) => {
|
||||||
const original = await importOriginal<typeof import("@deadhead/content")>();
|
const original = await importOriginal<typeof import("@deadhead/content")>();
|
||||||
return {
|
return {
|
||||||
|
|
@ -17,19 +17,18 @@ vi.mock("@deadhead/content", async (importOriginal) => {
|
||||||
const testApp = useTestApp();
|
const testApp = useTestApp();
|
||||||
|
|
||||||
describe("deal availability with tutorialRunsBeforeDeal = 3", () => {
|
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);
|
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({
|
expect((await getState(testApp().app, guest)).tutorial).toMatchObject({
|
||||||
runsCompleted: 3,
|
runsCompleted: 2,
|
||||||
loanerLocationId: "moon",
|
|
||||||
dealAvailable: false,
|
dealAvailable: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const state = await flyTutorialRun(testApp(), guest);
|
const state = await flyTutorialRun(testApp(), guest);
|
||||||
expect(state.tutorial).toMatchObject({
|
expect(state.tutorial).toMatchObject({
|
||||||
runsCompleted: 4,
|
runsCompleted: 3,
|
||||||
loanerLocationId: "earth",
|
loanerLocationId: "moon",
|
||||||
dealAvailable: true,
|
dealAvailable: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -55,20 +55,20 @@ describe("tutorial flow v2", () => {
|
||||||
expect(third.find((c) => c.scriptedId === "unmarked-crates")).toBeDefined();
|
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);
|
const guest = await signUp(testApp().app);
|
||||||
for (let run = 1; run <= 3; run++) await flyTutorialRun(testApp(), guest);
|
for (let run = 1; run <= 4; run++) await flyTutorialRun(testApp(), guest);
|
||||||
const afterThree = await getState(testApp().app, guest);
|
const afterFour = await getState(testApp().app, guest);
|
||||||
expect(afterThree.tutorial).toMatchObject({
|
|
||||||
runsCompleted: 3,
|
|
||||||
loanerLocationId: "moon",
|
|
||||||
dealAvailable: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const afterFour = await flyTutorialRun(testApp(), guest);
|
|
||||||
expect(afterFour.tutorial).toMatchObject({
|
expect(afterFour.tutorial).toMatchObject({
|
||||||
runsCompleted: 4,
|
runsCompleted: 4,
|
||||||
loanerLocationId: "earth",
|
loanerLocationId: "earth",
|
||||||
|
dealAvailable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const afterFive = await flyTutorialRun(testApp(), guest);
|
||||||
|
expect(afterFive.tutorial).toMatchObject({
|
||||||
|
runsCompleted: 5,
|
||||||
|
loanerLocationId: "moon",
|
||||||
runInFlight: false,
|
runInFlight: false,
|
||||||
dealAvailable: true,
|
dealAvailable: true,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -384,7 +384,8 @@ export function simulate(options: SimulationOptions): SimulationResult {
|
||||||
// --- Tutorial and the deal (server dispatch.ts, deal.ts). ---
|
// --- Tutorial and the deal (server dispatch.ts, deal.ts). ---
|
||||||
function playTutorial(): boolean {
|
function playTutorial(): boolean {
|
||||||
if (flights.some((f) => f.record.tutorial)) return false;
|
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();
|
acceptDeal();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue