generated from nhcarrigan/template
fix: real-time companion unlocks, persist exploration endsAt, correct auto-prestige formula, and quest balance
Closes #191 Closes #205 Closes #206 Closes #212 Closes #214 Closes #216 Closes #224
This commit is contained in:
@@ -77,7 +77,7 @@ export const defaultQuests: Array<Quest> = [
|
||||
combatPowerRequired: 500,
|
||||
description:
|
||||
"A rogue necromancer has raised an army of skeletons near the city. Silence him before the dead overrun us.",
|
||||
durationSeconds: 5 * 60,
|
||||
durationSeconds: 30 * 60,
|
||||
id: "necromancer_tower",
|
||||
name: "Necromancer's Tower",
|
||||
prerequisiteIds: [],
|
||||
@@ -94,7 +94,7 @@ export const defaultQuests: Array<Quest> = [
|
||||
combatPowerRequired: 2000,
|
||||
description:
|
||||
"An ancient fortress still garrisoned by constructs who don't know the war ended. Clear it out and claim its vaults.",
|
||||
durationSeconds: 5 * 60,
|
||||
durationSeconds: 45 * 60,
|
||||
id: "crumbling_fortress",
|
||||
name: "The Crumbling Fortress",
|
||||
prerequisiteIds: [ "necromancer_tower" ],
|
||||
@@ -111,7 +111,7 @@ export const defaultQuests: Array<Quest> = [
|
||||
combatPowerRequired: 8000,
|
||||
description:
|
||||
"A vast library sealed for centuries whose contents have warped and grown hostile. The knowledge within is priceless.",
|
||||
durationSeconds: 10 * 60,
|
||||
durationSeconds: 60 * 60,
|
||||
id: "cursed_library",
|
||||
name: "The Cursed Library",
|
||||
prerequisiteIds: [ "crumbling_fortress" ],
|
||||
@@ -127,7 +127,7 @@ export const defaultQuests: Array<Quest> = [
|
||||
combatPowerRequired: 30_000,
|
||||
description:
|
||||
"The legendary lair of Pyraxis the Undying. Few who enter return — those who do are rich beyond imagining.",
|
||||
durationSeconds: 15 * 60,
|
||||
durationSeconds: 90 * 60,
|
||||
id: "dragon_lair",
|
||||
name: "Dragon's Lair",
|
||||
prerequisiteIds: [ "cursed_library" ],
|
||||
@@ -545,7 +545,7 @@ export const defaultQuests: Array<Quest> = [
|
||||
rewards: [
|
||||
{ amount: 3_000_000_000_000, type: "gold" },
|
||||
{ amount: 1_500_000_000, type: "essence" },
|
||||
{ amount: 12_000_000, type: "crystals" },
|
||||
{ amount: 0, type: "crystals" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "abyssal_trench",
|
||||
@@ -561,7 +561,7 @@ export const defaultQuests: Array<Quest> = [
|
||||
rewards: [
|
||||
{ amount: 10_000_000_000_000, type: "gold" },
|
||||
{ amount: 5_000_000_000, type: "essence" },
|
||||
{ amount: 30_000_000, type: "crystals" },
|
||||
{ amount: 0, type: "crystals" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "abyssal_trench",
|
||||
@@ -577,7 +577,7 @@ export const defaultQuests: Array<Quest> = [
|
||||
rewards: [
|
||||
{ amount: 30_000_000_000_000, type: "gold" },
|
||||
{ amount: 15_000_000_000, type: "essence" },
|
||||
{ amount: 60_000_000, type: "crystals" },
|
||||
{ amount: 0, type: "crystals" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "abyssal_trench",
|
||||
@@ -593,7 +593,7 @@ export const defaultQuests: Array<Quest> = [
|
||||
rewards: [
|
||||
{ amount: 100_000_000_000_000, type: "gold" },
|
||||
{ amount: 50_000_000_000, type: "essence" },
|
||||
{ amount: 120_000_000, type: "crystals" },
|
||||
{ amount: 0, type: "crystals" },
|
||||
{ targetId: "abyss_diver_1", type: "upgrade" },
|
||||
],
|
||||
status: "locked",
|
||||
@@ -610,7 +610,7 @@ export const defaultQuests: Array<Quest> = [
|
||||
rewards: [
|
||||
{ amount: 400_000_000_000_000, type: "gold" },
|
||||
{ amount: 200_000_000_000, type: "essence" },
|
||||
{ amount: 400_000_000, type: "crystals" },
|
||||
{ amount: 0, type: "crystals" },
|
||||
],
|
||||
status: "locked",
|
||||
zoneId: "abyssal_trench",
|
||||
|
||||
@@ -191,17 +191,17 @@ exploreRouter.post("/start", async(context) => {
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
// eslint-disable-next-line stylistic/no-mixed-operators -- duration * 1000 is clear
|
||||
const endsAt = now + explorationArea.durationSeconds * 1000;
|
||||
area.status = "in_progress";
|
||||
area.startedAt = now;
|
||||
area.endsAt = endsAt;
|
||||
|
||||
await prisma.gameState.update({
|
||||
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Prisma requires object */
|
||||
data: { state: state as object, updatedAt: now },
|
||||
where: { discordId },
|
||||
});
|
||||
|
||||
// eslint-disable-next-line stylistic/no-mixed-operators -- duration * 1000 is clear
|
||||
const endsAt = now + explorationArea.durationSeconds * 1000;
|
||||
const response: ExploreStartResponse = {
|
||||
areaId,
|
||||
endsAt,
|
||||
|
||||
@@ -246,6 +246,22 @@ describe("explore route", () => {
|
||||
expect(body.endsAt).toBeGreaterThan(Date.now());
|
||||
});
|
||||
|
||||
it("persists endsAt to the DB state on exploration start", async () => {
|
||||
const state = makeState();
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await postStart({ areaId: TEST_AREA_ID });
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json() as { areaId: string; endsAt: number };
|
||||
const updateCall = vi.mocked(prisma.gameState.update).mock.calls[0]?.[0];
|
||||
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Test accesses nested mock data */
|
||||
const savedState = (updateCall?.data as { state?: { exploration?: { areas?: Array<{ id: string; endsAt?: number }> } } }).state;
|
||||
const savedArea = savedState?.exploration?.areas?.find((a) => {
|
||||
return a.id === TEST_AREA_ID;
|
||||
});
|
||||
expect(savedArea?.endsAt).toBe(body.endsAt);
|
||||
});
|
||||
|
||||
it("backfills exploration state for old saves without exploration", async () => {
|
||||
const state = makeState({ exploration: undefined });
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
|
||||
Reference in New Issue
Block a user