feat: vampire tick engine, auto systems, and full test suite

- vampire blood production tick with thrall bloodPerSecond + multipliers
- auto-quest and auto-thrall purchase in tick engine
- computeVampireBloodPerSecond helper exposed for ResourceBar display
- ResourceBar now shows blood/s and currency balances for vampire mode
- vampire quests and thralls panels gain auto-toggle buttons
- About page updated with vampire mode how-to-play entries
- vampireEquipmentSets data file added to web
- 100% test coverage across all API routes and services:
  - siring, awakening, vampireBoss, vampireCraft, vampireExplore, vampireUpgrade
  - debug route now covers grant-apotheosis endpoint
  - vampireMaterials excluded from coverage (ID-referenced only, same as goddessMaterials)
This commit is contained in:
2026-04-16 14:01:50 -07:00
committed by Naomi Carrigan
parent 1e0a7b142a
commit e02827dbb6
20 changed files with 3660 additions and 10 deletions
+61
View File
@@ -1206,6 +1206,67 @@ describe("debug route", () => {
});
});
describe("POST /grant-apotheosis", () => {
const grantApotheosis = () =>
app.fetch(new Request("http://localhost/debug/grant-apotheosis", { method: "POST" }));
it("returns 404 when no save is found", async () => {
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce(null);
const res = await grantApotheosis();
expect(res.status).toBe(404);
const body = await res.json() as { error: string };
expect(body.error).toContain("No save found");
});
it("returns 200 with unchanged state when apotheosis count is already >= 1", async () => {
const state = makeState({ apotheosis: { count: 1 } });
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
const res = await grantApotheosis();
expect(res.status).toBe(200);
const body = await res.json() as { state: GameState };
expect(body.state.apotheosis?.count).toBe(1);
expect(vi.mocked(prisma.gameState.update)).not.toHaveBeenCalled();
});
it("returns 200 and grants apotheosis with goddess state when not yet granted", async () => {
const state = makeState();
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await grantApotheosis();
expect(res.status).toBe(200);
const body = await res.json() as { state: GameState };
expect(body.state.apotheosis?.count).toBe(1);
expect(body.state.goddess).toBeDefined();
});
it("returns 200 with HMAC signature when ANTI_CHEAT_SECRET is set", async () => {
process.env.ANTI_CHEAT_SECRET = "test_secret";
const state = makeState({ apotheosis: { count: 1 } });
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
const res = await grantApotheosis();
expect(res.status).toBe(200);
const body = await res.json() as { signature: string | undefined };
expect(body.signature).toBeDefined();
delete process.env.ANTI_CHEAT_SECRET;
});
it("returns 500 when DB throws an Error", async () => {
vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce(new Error("DB error"));
const res = await grantApotheosis();
expect(res.status).toBe(500);
const body = await res.json() as { error: string };
expect(body.error).toContain("Internal server error");
});
it("returns 500 when DB throws a non-Error value", async () => {
vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce("raw error");
const res = await grantApotheosis();
expect(res.status).toBe(500);
const body = await res.json() as { error: string };
expect(body.error).toContain("Internal server error");
});
});
describe("POST /hard-reset", () => {
it("returns 404 when no player found", async () => {
vi.mocked(prisma.player.findUnique).mockResolvedValueOnce(null);