Feature: add town systems, saves, recovery, and level progression

This commit is contained in:
Keith Solomon
2026-03-18 19:37:01 -05:00
parent a4d2890cd9
commit 8600f611a6
22 changed files with 2434 additions and 16 deletions

View File

@@ -0,0 +1,126 @@
import { describe, expect, it } from "vitest";
import { sampleContentPack } from "@/data/sampleContentPack";
import { createStartingAdventurer } from "./character";
import { createRunState, returnToTown } from "./runState";
import {
grantDebugTreasure,
queueTreasureForSale,
sellPendingTreasure,
stashCarriedTreasure,
withdrawStashedTreasure,
} from "./townInventory";
function createAdventurer() {
return createStartingAdventurer(sampleContentPack, {
name: "Aster",
weaponId: "weapon.short-sword",
armourId: "armour.leather-vest",
scrollId: "scroll.lesser-heal",
});
}
function createTownRun() {
const run = createRunState({
content: sampleContentPack,
campaignId: "campaign.1",
adventurer: createAdventurer(),
});
return returnToTown(run, "2026-03-18T21:00:00.000Z").run;
}
describe("town inventory loop", () => {
it("stores carried treasure in the town stash", () => {
const inTown = createTownRun();
inTown.adventurerSnapshot.inventory.carried.push({
definitionId: "item.silver-chalice",
quantity: 1,
});
const result = stashCarriedTreasure({
content: sampleContentPack,
run: inTown,
definitionId: "item.silver-chalice",
at: "2026-03-18T21:05:00.000Z",
});
expect(result.run.adventurerSnapshot.inventory.carried).not.toEqual(
expect.arrayContaining([expect.objectContaining({ definitionId: "item.silver-chalice" })]),
);
expect(result.run.townState.stash).toEqual(
expect.arrayContaining([
expect.objectContaining({
definitionId: "item.silver-chalice",
quantity: 1,
}),
]),
);
});
it("withdraws stashed treasure back to the pack", () => {
const inTown = createTownRun();
inTown.townState.stash.push({
definitionId: "item.garnet-ring",
quantity: 1,
});
const result = withdrawStashedTreasure({
content: sampleContentPack,
run: inTown,
definitionId: "item.garnet-ring",
at: "2026-03-18T21:06:00.000Z",
});
expect(result.run.townState.stash).toHaveLength(0);
expect(result.run.adventurerSnapshot.inventory.carried).toEqual(
expect.arrayContaining([
expect.objectContaining({
definitionId: "item.garnet-ring",
quantity: 1,
}),
]),
);
});
it("queues treasure for sale and converts it into gold", () => {
const inTown = createTownRun();
const withTreasure = grantDebugTreasure({
content: sampleContentPack,
run: inTown,
definitionId: "item.garnet-ring",
quantity: 2,
at: "2026-03-18T21:07:00.000Z",
}).run;
const queued = queueTreasureForSale({
content: sampleContentPack,
run: withTreasure,
definitionId: "item.garnet-ring",
quantity: 2,
source: "carried",
at: "2026-03-18T21:08:00.000Z",
}).run;
const sold = sellPendingTreasure({
content: sampleContentPack,
run: queued,
at: "2026-03-18T21:09:00.000Z",
}).run;
expect(queued.townState.pendingSales).toEqual(
expect.arrayContaining([
expect.objectContaining({
definitionId: "item.garnet-ring",
quantity: 2,
}),
]),
);
expect(sold.townState.pendingSales).toHaveLength(0);
expect(sold.adventurerSnapshot.inventory.currency.gold).toBe(
withTreasure.adventurerSnapshot.inventory.currency.gold + 24,
);
expect(sold.log.at(-1)?.text).toContain("24 gold");
});
});