89 lines
3.1 KiB
TypeScript
89 lines
3.1 KiB
TypeScript
import { Component, type ErrorInfo, type ReactNode } from "react";
|
|
import { restartActiveDungeonSession } from "../game/dungeonSession";
|
|
import { useGameStore } from "../game/store";
|
|
|
|
interface Props { children: ReactNode }
|
|
interface State {
|
|
error: Error | null;
|
|
retryCount: number;
|
|
retryScheduled: boolean;
|
|
}
|
|
|
|
const MAX_AUTOMATIC_RETRIES = 1;
|
|
const AUTOMATIC_RETRY_DELAY_MS = 500;
|
|
const RETRY_BUDGET_RESET_MS = 60_000;
|
|
|
|
export class SceneErrorBoundary extends Component<Props, State> {
|
|
state: State = { error: null, retryCount: 0, retryScheduled: false };
|
|
private retryTimer: number | null = null;
|
|
private retryBudgetTimer: number | null = null;
|
|
|
|
static getDerivedStateFromError(error: Error): Partial<State> {
|
|
return { error };
|
|
}
|
|
|
|
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
console.error("Healer Man scene failed", error, info.componentStack);
|
|
useGameStore.getState().setAssetStatus("error", error.message);
|
|
if (this.retryBudgetTimer !== null) {
|
|
window.clearTimeout(this.retryBudgetTimer);
|
|
this.retryBudgetTimer = null;
|
|
}
|
|
if (this.state.retryCount < MAX_AUTOMATIC_RETRIES) this.scheduleRetry();
|
|
}
|
|
|
|
componentWillUnmount(): void {
|
|
if (this.retryTimer !== null) window.clearTimeout(this.retryTimer);
|
|
if (this.retryBudgetTimer !== null) window.clearTimeout(this.retryBudgetTimer);
|
|
}
|
|
|
|
private scheduleRetry(): void {
|
|
if (this.retryTimer !== null) return;
|
|
this.setState({ retryScheduled: true });
|
|
this.retryTimer = window.setTimeout(() => {
|
|
this.retryTimer = null;
|
|
this.retryScene();
|
|
}, AUTOMATIC_RETRY_DELAY_MS);
|
|
}
|
|
|
|
private retryScene = (): void => {
|
|
const retryCount = this.state.retryCount + 1;
|
|
try {
|
|
if (!restartActiveDungeonSession()) {
|
|
this.setState({ retryScheduled: false });
|
|
return;
|
|
}
|
|
this.setState({ error: null, retryCount, retryScheduled: false });
|
|
this.retryBudgetTimer = window.setTimeout(() => {
|
|
this.retryBudgetTimer = null;
|
|
if (!this.state.error) this.setState({ retryCount: 0 });
|
|
}, RETRY_BUDGET_RESET_MS);
|
|
} catch (error) {
|
|
const nextError = error instanceof Error ? error : new Error("The dungeon retry failed.");
|
|
console.error("Healer Man scene retry failed", nextError);
|
|
useGameStore.getState().setAssetStatus("error", nextError.message);
|
|
this.setState({ error: nextError, retryCount, retryScheduled: false });
|
|
}
|
|
}
|
|
|
|
render() {
|
|
if (!this.state.error) return this.props.children;
|
|
return (
|
|
<div className="scene-error" role="alert">
|
|
<p className="eyebrow">Dungeon load interrupted</p>
|
|
<h1>{this.state.retryScheduled ? "Recovering the expedition..." : "The cavern could not be assembled."}</h1>
|
|
<p>
|
|
{this.state.retryScheduled
|
|
? "Clearing the failed asset cache and rebuilding the dungeon in smaller batches."
|
|
: this.state.error.message}
|
|
</p>
|
|
{!this.state.retryScheduled && (
|
|
<button type="button" className="button button--primary" onClick={this.retryScene}>
|
|
Retry dungeon
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
}
|