generated from nhcarrigan/template
feat: goddess sync, sanitize, and apotheosis init (chunk 3)
- initialState: add initialGoddessState() with all goddess sub-objects - apotheosis: init GoddessState on first apotheosis, preserve on subsequent - game: add goddessSpread block in validateAndSanitize (server-only fields capped, forward-only boss/quest/achievement enforcement) - debug: add injectMissingGoddessExplorationAreas helper and inject all 8 goddess content arrays in syncNewContent - vitest.config.ts: remove 8 goddess data files from coverage exclude (now imported via initialState) - tests: full coverage for all new code (482 tests, 100% coverage)
This commit is contained in:
@@ -513,6 +513,188 @@ describe("game route", () => {
|
||||
const res = await save({ state: stateWithCompanion });
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it("passes through goddess when incoming has goddess and previous does not", async () => {
|
||||
const goddess: GameState["goddess"] = {
|
||||
achievements: [],
|
||||
baseClickPower: 1,
|
||||
bosses: [],
|
||||
consecration: { count: 0, divinity: 0, productionMultiplier: 1, purchasedUpgradeIds: [] },
|
||||
disciples: [],
|
||||
enlightenment: { count: 0, purchasedUpgradeIds: [], stardust: 0, stardustCombatMultiplier: 1, stardustConsecrationDivinityMultiplier: 1, stardustConsecrationThresholdMultiplier: 1, stardustMetaMultiplier: 1, stardustPrayersMultiplier: 1 },
|
||||
equipment: [],
|
||||
exploration: { areas: [], craftedCombatMultiplier: 1, craftedDivinityMultiplier: 1, craftedPrayersMultiplier: 1, craftedRecipeIds: [], materials: [] },
|
||||
lastTickAt: 0,
|
||||
lifetimeBossesDefeated: 0,
|
||||
lifetimePrayersEarned: 0,
|
||||
lifetimeQuestsCompleted: 0,
|
||||
quests: [],
|
||||
totalPrayersEarned: 0,
|
||||
upgrades: [],
|
||||
zones: [],
|
||||
};
|
||||
const prevState = makeState();
|
||||
const incomingState = makeState({ goddess });
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state: prevState } as never);
|
||||
vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer() as never);
|
||||
vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never);
|
||||
vi.mocked(prisma.gameState.upsert).mockResolvedValueOnce({} as never);
|
||||
const res = await save({ state: incomingState });
|
||||
expect(res.status).toBe(200);
|
||||
const savedState = (vi.mocked(prisma.gameState.upsert).mock.calls[0][0] as unknown as { create: { state: GameState } }).create.state;
|
||||
expect(savedState.goddess).toBeDefined();
|
||||
});
|
||||
|
||||
it("does not add goddess to result when neither previous nor incoming has goddess", async () => {
|
||||
const prevState = makeState();
|
||||
const incomingState = makeState();
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state: prevState } as never);
|
||||
vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer() as never);
|
||||
vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never);
|
||||
vi.mocked(prisma.gameState.upsert).mockResolvedValueOnce({} as never);
|
||||
const res = await save({ state: incomingState });
|
||||
expect(res.status).toBe(200);
|
||||
const savedState = (vi.mocked(prisma.gameState.upsert).mock.calls[0][0] as unknown as { create: { state: GameState } }).create.state;
|
||||
expect(savedState.goddess).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves previous goddess when incoming save lacks goddess", async () => {
|
||||
const goddess: GameState["goddess"] = {
|
||||
achievements: [],
|
||||
baseClickPower: 1,
|
||||
bosses: [],
|
||||
consecration: { count: 0, divinity: 0, productionMultiplier: 1, purchasedUpgradeIds: [] },
|
||||
disciples: [],
|
||||
enlightenment: { count: 0, purchasedUpgradeIds: [], stardust: 0, stardustCombatMultiplier: 1, stardustConsecrationDivinityMultiplier: 1, stardustConsecrationThresholdMultiplier: 1, stardustMetaMultiplier: 1, stardustPrayersMultiplier: 1 },
|
||||
equipment: [],
|
||||
exploration: { areas: [], craftedCombatMultiplier: 1, craftedDivinityMultiplier: 1, craftedPrayersMultiplier: 1, craftedRecipeIds: [], materials: [] },
|
||||
lastTickAt: 0,
|
||||
lifetimeBossesDefeated: 0,
|
||||
lifetimePrayersEarned: 0,
|
||||
lifetimeQuestsCompleted: 0,
|
||||
quests: [],
|
||||
totalPrayersEarned: 0,
|
||||
upgrades: [],
|
||||
zones: [],
|
||||
};
|
||||
const prevState = makeState({ goddess });
|
||||
const incomingState = makeState();
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state: prevState } as never);
|
||||
vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer() as never);
|
||||
vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never);
|
||||
vi.mocked(prisma.gameState.upsert).mockResolvedValueOnce({} as never);
|
||||
const res = await save({ state: incomingState });
|
||||
expect(res.status).toBe(200);
|
||||
const savedState = (vi.mocked(prisma.gameState.upsert).mock.calls[0][0] as unknown as { create: { state: GameState } }).create.state;
|
||||
expect(savedState.goddess).toEqual(goddess);
|
||||
});
|
||||
|
||||
it("caps goddess totalPrayersEarned at previous value (server-only field)", async () => {
|
||||
const prevGoddess: GameState["goddess"] = {
|
||||
achievements: [],
|
||||
baseClickPower: 1,
|
||||
bosses: [],
|
||||
consecration: { count: 0, divinity: 0, productionMultiplier: 1, purchasedUpgradeIds: [] },
|
||||
disciples: [],
|
||||
enlightenment: { count: 0, purchasedUpgradeIds: [], stardust: 0, stardustCombatMultiplier: 1, stardustConsecrationDivinityMultiplier: 1, stardustConsecrationThresholdMultiplier: 1, stardustMetaMultiplier: 1, stardustPrayersMultiplier: 1 },
|
||||
equipment: [],
|
||||
exploration: { areas: [], craftedCombatMultiplier: 1, craftedDivinityMultiplier: 1, craftedPrayersMultiplier: 1, craftedRecipeIds: [], materials: [] },
|
||||
lastTickAt: 0,
|
||||
lifetimeBossesDefeated: 0,
|
||||
lifetimePrayersEarned: 0,
|
||||
lifetimeQuestsCompleted: 0,
|
||||
quests: [],
|
||||
totalPrayersEarned: 100,
|
||||
upgrades: [],
|
||||
zones: [],
|
||||
};
|
||||
const incomingGoddess: GameState["goddess"] = {
|
||||
...prevGoddess,
|
||||
totalPrayersEarned: 9999,
|
||||
};
|
||||
const prevState = makeState({ goddess: prevGoddess });
|
||||
const incomingState = makeState({ goddess: incomingGoddess });
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state: prevState } as never);
|
||||
vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer() as never);
|
||||
vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never);
|
||||
vi.mocked(prisma.gameState.upsert).mockResolvedValueOnce({} as never);
|
||||
const res = await save({ state: incomingState });
|
||||
expect(res.status).toBe(200);
|
||||
const savedState = (vi.mocked(prisma.gameState.upsert).mock.calls[0][0] as unknown as { create: { state: GameState } }).create.state;
|
||||
expect(savedState.goddess?.totalPrayersEarned).toBe(100);
|
||||
});
|
||||
|
||||
it("restores goddess boss defeated status when incoming tries to un-defeat it", async () => {
|
||||
const prevGoddess: GameState["goddess"] = {
|
||||
achievements: [],
|
||||
baseClickPower: 1,
|
||||
bosses: [{ id: "divine_sentinel", status: "defeated", currentHp: 0, maxHp: 5000 }] as GameState["goddess"]["bosses"],
|
||||
consecration: { count: 0, divinity: 0, productionMultiplier: 1, purchasedUpgradeIds: [] },
|
||||
disciples: [],
|
||||
enlightenment: { count: 0, purchasedUpgradeIds: [], stardust: 0, stardustCombatMultiplier: 1, stardustConsecrationDivinityMultiplier: 1, stardustConsecrationThresholdMultiplier: 1, stardustMetaMultiplier: 1, stardustPrayersMultiplier: 1 },
|
||||
equipment: [],
|
||||
exploration: { areas: [], craftedCombatMultiplier: 1, craftedDivinityMultiplier: 1, craftedPrayersMultiplier: 1, craftedRecipeIds: [], materials: [] },
|
||||
lastTickAt: 0,
|
||||
lifetimeBossesDefeated: 0,
|
||||
lifetimePrayersEarned: 0,
|
||||
lifetimeQuestsCompleted: 0,
|
||||
quests: [],
|
||||
totalPrayersEarned: 0,
|
||||
upgrades: [],
|
||||
zones: [],
|
||||
};
|
||||
const incomingGoddess: GameState["goddess"] = {
|
||||
...prevGoddess,
|
||||
bosses: [{ id: "divine_sentinel", status: "available", currentHp: 5000, maxHp: 5000 }] as GameState["goddess"]["bosses"],
|
||||
};
|
||||
const prevState = makeState({ goddess: prevGoddess });
|
||||
const incomingState = makeState({ goddess: incomingGoddess });
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state: prevState } as never);
|
||||
vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer() as never);
|
||||
vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never);
|
||||
vi.mocked(prisma.gameState.upsert).mockResolvedValueOnce({} as never);
|
||||
const res = await save({ state: incomingState });
|
||||
expect(res.status).toBe(200);
|
||||
const savedState = (vi.mocked(prisma.gameState.upsert).mock.calls[0][0] as unknown as { create: { state: GameState } }).create.state;
|
||||
const boss = savedState.goddess?.bosses.find((b) => b.id === "divine_sentinel");
|
||||
expect(boss?.status).toBe("defeated");
|
||||
});
|
||||
|
||||
it("restores goddess quest completed status when incoming tries to un-complete it", async () => {
|
||||
const prevGoddess: GameState["goddess"] = {
|
||||
achievements: [],
|
||||
baseClickPower: 1,
|
||||
bosses: [],
|
||||
consecration: { count: 0, divinity: 0, productionMultiplier: 1, purchasedUpgradeIds: [] },
|
||||
disciples: [],
|
||||
enlightenment: { count: 0, purchasedUpgradeIds: [], stardust: 0, stardustCombatMultiplier: 1, stardustConsecrationDivinityMultiplier: 1, stardustConsecrationThresholdMultiplier: 1, stardustMetaMultiplier: 1, stardustPrayersMultiplier: 1 },
|
||||
equipment: [],
|
||||
exploration: { areas: [], craftedCombatMultiplier: 1, craftedDivinityMultiplier: 1, craftedPrayersMultiplier: 1, craftedRecipeIds: [], materials: [] },
|
||||
lastTickAt: 0,
|
||||
lifetimeBossesDefeated: 0,
|
||||
lifetimePrayersEarned: 0,
|
||||
lifetimeQuestsCompleted: 0,
|
||||
quests: [{ id: "offering_ritual", status: "completed" }] as GameState["goddess"]["quests"],
|
||||
totalPrayersEarned: 0,
|
||||
upgrades: [],
|
||||
zones: [],
|
||||
};
|
||||
const incomingGoddess: GameState["goddess"] = {
|
||||
...prevGoddess,
|
||||
quests: [{ id: "offering_ritual", status: "available" }] as GameState["goddess"]["quests"],
|
||||
};
|
||||
const prevState = makeState({ goddess: prevGoddess });
|
||||
const incomingState = makeState({ goddess: incomingGoddess });
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state: prevState } as never);
|
||||
vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(makePlayer() as never);
|
||||
vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never);
|
||||
vi.mocked(prisma.gameState.upsert).mockResolvedValueOnce({} as never);
|
||||
const res = await save({ state: incomingState });
|
||||
expect(res.status).toBe(200);
|
||||
const savedState = (vi.mocked(prisma.gameState.upsert).mock.calls[0][0] as unknown as { create: { state: GameState } }).create.state;
|
||||
const quest = savedState.goddess?.quests.find((q) => q.id === "offering_ritual");
|
||||
expect(quest?.status).toBe("completed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /load error path", () => {
|
||||
|
||||
Reference in New Issue
Block a user