feat: poll server for exploration claimability before showing collect button

Resolves #127
This commit is contained in:
2026-03-24 12:00:11 -07:00
committed by Naomi Carrigan
parent 0c7a5f50fc
commit b85126c345
5 changed files with 149 additions and 3 deletions
+15
View File
@@ -17,6 +17,7 @@ import type {
BuyPrestigeUpgradeResponse,
CraftRecipeRequest,
CraftRecipeResponse,
ExploreClaimableResponse,
ExploreCollectRequest,
ExploreCollectResponse,
ExploreStartRequest,
@@ -244,6 +245,19 @@ const collectExploration = async(
});
};
/**
* Checks whether a given exploration area is ready to claim on the server.
* @param areaId - The area ID to check.
* @returns Whether the exploration is claimable.
*/
const checkExplorationClaimable = async(
areaId: string,
): Promise<ExploreClaimableResponse> => {
return await fetchJson<ExploreClaimableResponse>(
`/explore/claimable?areaId=${encodeURIComponent(areaId)}`,
);
};
/**
* Crafts a recipe on the server.
* @param body - The craft recipe request payload.
@@ -316,6 +330,7 @@ export {
buyEchoUpgrade,
buyPrestigeUpgrade,
challengeBoss,
checkExplorationClaimable,
collectExploration,
craftRecipe,
debugHardReset,
@@ -7,12 +7,17 @@
/* eslint-disable max-lines-per-function -- Complex component with many render paths */
/* eslint-disable complexity -- Complex component with many conditional render paths */
/* eslint-disable max-lines -- Exploration panel requires many render paths and result display */
import { type JSX, useState } from "react";
/* eslint-disable max-statements -- Component function requires many state declarations and handlers */
import { type JSX, useEffect, useRef, useState } from "react";
import { checkExplorationClaimable } from "../../api/client.js";
import { useGame } from "../../context/gameContext.js";
import { EXPLORATION_AREAS } from "../../data/explorations.js";
import { cdnImage } from "../../utils/cdn.js";
import { ZoneSelector } from "./zoneSelector.js";
import type { ExploreCollectResponse } from "@elysium/types";
import type {
ExploreClaimableResponse,
ExploreCollectResponse,
} from "@elysium/types";
/**
* Formats a duration in seconds to a human-readable string.
@@ -83,6 +88,61 @@ const ExplorationPanel = (): JSX.Element => {
});
const [ pendingAreaId, setPendingAreaId ] = useState<string | null>(null);
const [ lastResult, setLastResult ] = useState<CollectResult | null>(null);
const [ claimableAreaIds, setClaimableAreaIds ]
= useState<ReadonlySet<string>>(new Set());
const stateReference = useRef(state);
stateReference.current = state;
const claimableReference = useRef(claimableAreaIds);
claimableReference.current = claimableAreaIds;
useEffect(() => {
const pollClaimable = async(): Promise<void> => {
const currentState = stateReference.current;
if (currentState === null) {
return;
}
const inProgressArea = currentState.exploration?.areas.find((a) => {
return a.status === "in_progress";
});
if (inProgressArea === undefined) {
return;
}
if (claimableReference.current.has(inProgressArea.id)) {
return;
}
const areaData = EXPLORATION_AREAS.find((a) => {
return a.id === inProgressArea.id;
});
if (areaData === undefined) {
return;
}
const remaining = timeRemaining(
inProgressArea.endsAt,
inProgressArea.startedAt ?? 0,
areaData.durationSeconds,
);
if (remaining > 0) {
return;
}
const result: ExploreClaimableResponse
= await checkExplorationClaimable(inProgressArea.id);
if (result.claimable) {
setClaimableAreaIds((previous) => {
return new Set([ ...previous, inProgressArea.id ]);
});
}
};
const intervalId = setInterval(() => {
void pollClaimable();
}, 1000);
return (): void => {
clearInterval(intervalId);
};
}, []);
if (state === null) {
return (
@@ -134,6 +194,11 @@ const ExplorationPanel = (): JSX.Element => {
try {
const result = await collectExploration(areaId);
setLastResult({ areaId: areaId, response: result });
setClaimableAreaIds((previous) => {
const next = new Set(previous);
next.delete(areaId);
return next;
});
} finally {
setPendingAreaId(null);
}
@@ -269,7 +334,7 @@ const ExplorationPanel = (): JSX.Element => {
const endsAt = areaState?.endsAt;
const isReady
= status === "in_progress"
&& timeRemaining(endsAt, startedAt, area.durationSeconds) <= 0;
&& claimableAreaIds.has(area.id);
const isPending = pendingAreaId === area.id;
function handleStartClick(): void {