generated from nhcarrigan/template
fix: use server-computed endsAt for exploration timer to prevent clock drift
Exploration timers were showing more time than the area's stated duration when the server clock was ahead of the client clock. The timer now uses the server-provided endsAt timestamp directly instead of deriving startedAt from it, making countdowns immune to client/server clock skew. Old saves without endsAt fall back to the previous startedAt-based calculation. Closes #53
This commit is contained in:
@@ -46,11 +46,21 @@ const formatDuration = (seconds: number): string => {
|
||||
|
||||
/**
|
||||
* Computes the time remaining for an exploration in progress.
|
||||
* Uses endsAt (server-computed) when available to avoid client/server clock drift.
|
||||
* Falls back to startedAt + durationSeconds for saves predating the endsAt field.
|
||||
* @param endsAt - The server-computed completion timestamp, if available.
|
||||
* @param startedAt - The timestamp when exploration started.
|
||||
* @param durationSeconds - The total duration in seconds.
|
||||
* @returns The remaining seconds.
|
||||
*/
|
||||
const timeRemaining = (startedAt: number, durationSeconds: number): number => {
|
||||
const timeRemaining = (
|
||||
endsAt: number | undefined,
|
||||
startedAt: number,
|
||||
durationSeconds: number,
|
||||
): number => {
|
||||
if (endsAt !== undefined) {
|
||||
return Math.max(0, (endsAt - Date.now()) / 1000);
|
||||
}
|
||||
const elapsed = (Date.now() - startedAt) / 1000;
|
||||
return Math.max(0, durationSeconds - elapsed);
|
||||
};
|
||||
@@ -217,9 +227,10 @@ const ExplorationPanel = (): JSX.Element => {
|
||||
});
|
||||
const status = areaState?.status ?? "locked";
|
||||
const startedAt = areaState?.startedAt ?? 0;
|
||||
const endsAt = areaState?.endsAt;
|
||||
const isReady
|
||||
= status === "in_progress"
|
||||
&& timeRemaining(startedAt, area.durationSeconds) <= 0;
|
||||
&& timeRemaining(endsAt, startedAt, area.durationSeconds) <= 0;
|
||||
const isPending = pendingAreaId === area.id;
|
||||
|
||||
function handleStartClick(): void {
|
||||
@@ -276,9 +287,8 @@ const ExplorationPanel = (): JSX.Element => {
|
||||
{status === "in_progress" && !isReady
|
||||
&& <span className="quest-badge active">
|
||||
{"⏳ "}
|
||||
{formatDuration(
|
||||
Math.ceil(timeRemaining(startedAt, area.durationSeconds)),
|
||||
)}
|
||||
{/* eslint-disable-next-line stylistic/max-len -- long call cannot be shortened */}
|
||||
{formatDuration(Math.ceil(timeRemaining(endsAt, startedAt, area.durationSeconds)))}
|
||||
{" remaining"}
|
||||
</span>
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ import {
|
||||
transcend as transcendApi,
|
||||
} from "../api/client.js";
|
||||
import { CODEX_ENTRIES } from "../data/codex.js";
|
||||
import { EXPLORATION_AREAS } from "../data/explorations.js";
|
||||
import { RECIPES } from "../data/recipes.js";
|
||||
import {
|
||||
RESOURCE_CAP,
|
||||
@@ -1671,14 +1670,6 @@ export const GameProvider = ({
|
||||
const startExploration = useCallback(async(areaId: string) => {
|
||||
try {
|
||||
const response = await startExplorationApi({ areaId });
|
||||
const areaData = EXPLORATION_AREAS.find((a) => {
|
||||
return a.id === areaId;
|
||||
});
|
||||
if (areaData === undefined) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line stylistic/no-mixed-operators -- duration * 1000 is clear
|
||||
const startedAt = response.endsAt - areaData.durationSeconds * 1000;
|
||||
setState((previous) => {
|
||||
if (previous?.exploration === undefined) {
|
||||
return previous;
|
||||
@@ -1689,7 +1680,11 @@ export const GameProvider = ({
|
||||
...previous.exploration,
|
||||
areas: previous.exploration.areas.map((a) => {
|
||||
return a.id === areaId
|
||||
? { ...a, startedAt: startedAt, status: "in_progress" as const }
|
||||
? {
|
||||
...a,
|
||||
endsAt: response.endsAt,
|
||||
status: "in_progress" as const,
|
||||
}
|
||||
: a;
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -72,6 +72,12 @@ interface ExplorationAreaState {
|
||||
*/
|
||||
startedAt?: number;
|
||||
|
||||
/**
|
||||
* Unix timestamp when the exploration will complete (server-computed, used for
|
||||
* accurate client-side countdown that is immune to client/server clock drift).
|
||||
*/
|
||||
endsAt?: number;
|
||||
|
||||
/**
|
||||
* True after the first successful collect — used for codex unlock detection.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user