Update Healer Man server
This commit is contained in:
1 parent
8a98797fcf
commit
11934a3979
44 files changed
+3183
-329
No files matched your search
@@ -6,7 +6,7 @@ import "./dungeonRuns.css";
|
||||
|
||||
export function DungeonLeaderboard({ dungeonTitle, data, scope, loading, filter, onFilter,
|
||||
expanded, onExpand, offset, onPage, onRefresh, difficultyLabel, onDifficulty,
|
||||
selectedId, select }: {
|
||||
bossLabel, onBoss, selectedId, select }: {
|
||||
readonly dungeonTitle: string;
|
||||
readonly data: LeaderboardData;
|
||||
readonly scope: LeaderboardScope;
|
||||
@@ -20,6 +20,8 @@ export function DungeonLeaderboard({ dungeonTitle, data, scope, loading, filter,
|
||||
readonly onRefresh: () => void;
|
||||
readonly difficultyLabel: string;
|
||||
readonly onDifficulty?: () => void;
|
||||
readonly bossLabel?: string;
|
||||
readonly onBoss?: () => void;
|
||||
readonly selectedId: string;
|
||||
readonly select: (id: string) => void;
|
||||
}) {
|
||||
@@ -35,6 +37,8 @@ export function DungeonLeaderboard({ dungeonTitle, data, scope, loading, filter,
|
||||
onClick={() => onFilter("full")}>Full dungeon clears <small>All bosses</small></ControllerButton>
|
||||
</div>
|
||||
<div className="dungeon-leaderboard__meta">
|
||||
{onBoss ? <ControllerButton {...controls} controlId="leaderboard-boss" onClick={onBoss}
|
||||
aria-label={`Leaderboard boss: ${bossLabel}. Change boss`}>Boss: {bossLabel} ↻</ControllerButton> : null}
|
||||
{onDifficulty ? <ControllerButton {...controls} controlId="leaderboard-difficulty" onClick={onDifficulty}
|
||||
aria-label={`Leaderboard difficulty: ${difficultyLabel}. Change difficulty`}>{difficultyLabel} ↻</ControllerButton>
|
||||
: <span>{difficultyLabel}</span>}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { type RaceBotSource, type RaceBotTier } from "../app/dungeonRaceBot";
|
||||
import { dungeonsByCategory, randomizedBiteSizedDungeons } from "../game/dungeonCatalog";
|
||||
import { dungeonEnabledForQueue } from "../game/dungeonQueuePolicy";
|
||||
import { formatDungeonDuration } from "../game/dungeonRunTypes";
|
||||
import { dungeonLeaderboardCatalog as leaderboardCatalog } from "../game/dungeonLeaderboardCatalog";
|
||||
import { partyRolesForClass, PARTY_ROLE_LABELS, type PartyRole } from "../game/partyStore";
|
||||
import { specializationById } from "../game/specializationCatalog";
|
||||
import { BrandMark, FrontSurface } from "./FrontSurface";
|
||||
@@ -31,6 +32,7 @@ export function DungeonRaceScreen({ onClose }: { readonly onClose: () => void })
|
||||
const [period, setPeriod] = useState<"30d" | "all">("30d");
|
||||
const [boardScope, setBoardScope] = useState<"overall" | "dungeon">("overall");
|
||||
const [boardView, setBoardView] = useState<"wins" | "times">("wins");
|
||||
const [boardBossId, setBoardBossId] = useState("");
|
||||
const [board, setBoard] = useState<BoardResponse | null>(null);
|
||||
const [boardError, setBoardError] = useState("");
|
||||
useEffect(() => { void refreshPolicy(session); }, [refreshPolicy, session]);
|
||||
@@ -38,17 +40,22 @@ export function DungeonRaceScreen({ onClose }: { readonly onClose: () => void })
|
||||
: dungeonsByCategory("bite-sized")).filter((entry) => entry.available && dungeonEnabledForQueue(policy, entry)),
|
||||
[pool, policy]);
|
||||
const boardDungeonId = dungeonId || allEntries[0]?.id || "";
|
||||
const boardDungeon = leaderboardCatalog.dungeons.find((entry) => entry.id === boardDungeonId);
|
||||
const bossOptions = boardDungeon?.difficulties.find((entry) => entry.id === boardDungeon.defaultDifficultyId)
|
||||
?.finalBossIds?.map((id) => boardDungeon.bosses.find((boss) => boss.id === id)!) ?? [];
|
||||
const selectedBoardBossId = bossOptions.find((boss) => boss.id === boardBossId)?.id ?? bossOptions[0]?.id;
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
const query = new URLSearchParams({ view: boardScope === "overall" ? "wins" : boardView,
|
||||
period, ...(boardScope === "dungeon" && boardDungeonId ? { dungeon: boardDungeonId } : {}) });
|
||||
period, ...(boardScope === "dungeon" && boardDungeonId ? { dungeon: boardDungeonId,
|
||||
...(selectedBoardBossId ? { boss: selectedBoardBossId } : {}) } : {}) });
|
||||
if (boardScope === "dungeon" && !boardDungeonId) { setBoard(null); return; }
|
||||
void requestOnlineJson<BoardResponse>(`/api/dungeon-races/leaderboard?${query}`, { signal: controller.signal })
|
||||
.then((value) => { setBoard(value); setBoardError(""); })
|
||||
.catch((error: unknown) => { if (!controller.signal.aborted) { setBoard(null);
|
||||
setBoardError(error instanceof Error ? error.message : "Leaderboard unavailable."); } });
|
||||
return () => controller.abort();
|
||||
}, [boardDungeonId, boardScope, boardView, period]);
|
||||
}, [boardDungeonId, boardScope, boardView, period, selectedBoardBossId]);
|
||||
const specialization = specializationById(character?.specializationId);
|
||||
const roles = character?.categoryId === "rom" ? partyRolesForClass(character.classId)
|
||||
: specialization ? [specialization.role] : [];
|
||||
@@ -80,8 +87,8 @@ export function DungeonRaceScreen({ onClose }: { readonly onClose: () => void })
|
||||
<button className="front-button" type="button" aria-pressed={pool === "randomized"} disabled={busy}
|
||||
onClick={() => { setPool("randomized"); setDungeonId(""); }}>Randomized Bite-Sized</button>
|
||||
</div>
|
||||
<label>Dungeon<select value={dungeonId} disabled={busy} onChange={(event) => setDungeonId(event.target.value)}>
|
||||
<option value="">Random eligible dungeon</option>
|
||||
<label>{pool === "randomized" ? "Creature pool" : "Dungeon"}<select value={dungeonId} disabled={busy} onChange={(event) => setDungeonId(event.target.value)}>
|
||||
<option value="">{pool === "randomized" ? "Random available creature pool" : "Random eligible dungeon"}</option>
|
||||
{allEntries.map((entry) => <option key={entry.id} value={entry.id}>{entry.title}</option>)}
|
||||
</select></label>
|
||||
<div className="dungeon-race-screen__choices" role="group" aria-label="Your role">
|
||||
@@ -116,6 +123,10 @@ export function DungeonRaceScreen({ onClose }: { readonly onClose: () => void })
|
||||
<button className="front-button" type="button" aria-pressed={boardScope === "dungeon"} onClick={() => setBoardScope("dungeon")}>This dungeon</button></div>
|
||||
{boardScope === "dungeon" && <div className="dungeon-race-screen__choices"><button className="front-button" type="button" aria-pressed={boardView === "wins"} onClick={() => setBoardView("wins")}>Wins & losses</button>
|
||||
<button className="front-button" type="button" aria-pressed={boardView === "times"} onClick={() => setBoardView("times")}>Fastest clears</button></div>}
|
||||
{boardScope === "dungeon" && bossOptions.length > 1 && <label>Boss<select value={selectedBoardBossId}
|
||||
onChange={(event) => setBoardBossId(event.target.value)}>
|
||||
{bossOptions.map((boss) => <option key={boss.id} value={boss.id}>{boss.name}</option>)}
|
||||
</select></label>}
|
||||
<div className="dungeon-race-screen__choices"><button className="front-button" type="button" aria-pressed={period === "30d"} onClick={() => setPeriod("30d")}>Last 30 days</button>
|
||||
<button className="front-button" type="button" aria-pressed={period === "all"} onClick={() => setPeriod("all")}>All time</button></div>
|
||||
{boardError && <p role="status">{boardError}</p>}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useMenuController, type MenuAction } from "../input/useMenuController";
|
||||
import { LazyLocalCompanionPanel } from "./LazyLocalCompanionPanel";
|
||||
import { ControllerButton } from "./ControllerButton";
|
||||
import { DungeonLeaderboard } from "./DungeonLeaderboard";
|
||||
import { dungeonLeaderboardCatalog as leaderboardCatalog } from "../game/dungeonLeaderboardCatalog";
|
||||
import { useDungeonLeaderboard } from "./useDungeonLeaderboard";
|
||||
import type { DungeonClearFilter } from "../game/dungeonRunTypes";
|
||||
import { DUNGEON_LEADERBOARD_PAGE_SIZE } from "../app/dungeonLeaderboardClient";
|
||||
@@ -89,13 +90,23 @@ export function DungeonSelectScreen() {
|
||||
const [clearFilter, setClearFilter] = useState<DungeonClearFilter>("final");
|
||||
const [difficultyId, setDifficultyId] = useState("");
|
||||
const [bossDifficulty, setBossDifficulty] = useState(1);
|
||||
const [leaderboardBossId, setLeaderboardBossId] = useState("");
|
||||
const includeBetaSpecs = useCompanionPartyPreferences((state) => state.includeBetaSpecs);
|
||||
const [expandedRuns, setExpandedRuns] = useState<ReadonlySet<string>>(new Set());
|
||||
const [leaderboardOpen, setLeaderboardOpen] = useState(false);
|
||||
const [readinessOpen, setReadinessOpen] = useState(false);
|
||||
const leaderboardDifficulty = selectedDefinition?.difficultyVariants.find((variant) => variant.id === difficultyId)
|
||||
?? selectedDefinition?.difficultyVariants.find((variant) => variant.id === selectedDefinition.defaultDifficultyId);
|
||||
const leaderboard = useDungeonLeaderboard(selectedDungeon?.id ?? "", leaderboardDifficulty?.id ?? "normal", clearFilter, bossDifficulty);
|
||||
const bossOptions = leaderboardCatalog.dungeons.find((entry) => entry.id === selectedDungeon?.id)?.bosses ?? [];
|
||||
const hasBossBoards = (leaderboardCatalog.dungeons.find((entry) => entry.id === selectedDungeon?.id)?.difficulties
|
||||
.find((entry) => entry.id === leaderboardDifficulty?.id)?.finalBossIds?.length ?? 0) > 1;
|
||||
const selectedBoss = hasBossBoards ? bossOptions.find((boss) => boss.id === leaderboardBossId) ?? bossOptions[0] : undefined;
|
||||
const cycleBoss = () => {
|
||||
const index = bossOptions.findIndex((boss) => boss.id === selectedBoss?.id);
|
||||
if (hasBossBoards && bossOptions.length) setLeaderboardBossId(bossOptions[(index + 1) % bossOptions.length].id);
|
||||
};
|
||||
const leaderboard = useDungeonLeaderboard(selectedDungeon?.id ?? "", leaderboardDifficulty?.id ?? "normal",
|
||||
clearFilter, bossDifficulty, selectedBoss?.id);
|
||||
const toggleRun = (id: string) => setExpandedRuns((current) => {
|
||||
const next = new Set(current); if (next.has(id)) next.delete(id); else next.add(id); return next;
|
||||
});
|
||||
@@ -213,6 +224,7 @@ export function DungeonSelectScreen() {
|
||||
{ id: "leaderboard-final", run: () => setClearFilter("final") },
|
||||
{ id: "leaderboard-full", run: () => setClearFilter("full") },
|
||||
{ id: "leaderboard-difficulty", run: cycleDifficulty, enabled: (selectedDefinition?.difficultyVariants.length ?? 0) > 1 },
|
||||
{ id: "leaderboard-boss", run: cycleBoss, enabled: hasBossBoards },
|
||||
{ id: "leaderboard-refresh", run: leaderboard.refresh },
|
||||
] : []),
|
||||
{ id: "boss-difficulty-down", run: () => setBossDifficulty((value) => Math.max(1, value - 1)), enabled: bossDifficulty > 1 },
|
||||
@@ -239,7 +251,7 @@ export function DungeonSelectScreen() {
|
||||
run: () => setRolePickerOpen(false),
|
||||
}] : []),
|
||||
{ id: "dungeon-back", run: returnToMainMenu },
|
||||
], [availableRoles, categoryDungeons, downloading, firstCategoryDungeon, pendingDownload, returnToMainMenu, rolePickerOpen, selectedCategory, selectedDungeon, selectedDefinition, leaderboard, leaderboardOpen, expandedRuns, leaderboardDifficulty, bossDifficulty]);
|
||||
], [availableRoles, categoryDungeons, downloading, firstCategoryDungeon, pendingDownload, returnToMainMenu, rolePickerOpen, selectedCategory, selectedDungeon, selectedDefinition, leaderboard, leaderboardOpen, expandedRuns, leaderboardDifficulty, bossDifficulty, hasBossBoards, selectedBoss]);
|
||||
const controller = useMenuController(actions, {
|
||||
initialId: firstAvailable ? `dungeon-${firstAvailable.id}` : "dungeon-back",
|
||||
onBack: pendingDownload
|
||||
@@ -426,6 +438,7 @@ export function DungeonSelectScreen() {
|
||||
expanded={expandedRuns} onExpand={toggleRun} offset={leaderboard.offset} onPage={leaderboard.setOffset}
|
||||
onRefresh={leaderboard.refresh} difficultyLabel={`${bossDifficultyLabel(bossDifficulty)} · ${leaderboardDifficulty?.difficulty === "heroic" ? "Heroic" : "Normal"}`}
|
||||
onDifficulty={(selectedDefinition?.difficultyVariants.length ?? 0) > 1 ? cycleDifficulty : undefined}
|
||||
bossLabel={selectedBoss?.name} onBoss={hasBossBoards ? cycleBoss : undefined}
|
||||
selectedId={controller.selectedId} select={controller.select} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -8,12 +8,13 @@ import { dungeonContentIsUsable, installContentPack } from "../content/contentMa
|
||||
import { useContentManagerSnapshot } from "../content/useContentManager";
|
||||
import {
|
||||
RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID,
|
||||
RANDOMIZED_BITE_SIZED_AVAILABLE_DUNGEON_ID,
|
||||
dungeonContentPackId,
|
||||
} from "../game/dungeonContentSource";
|
||||
import { dungeonById, randomRandomizedBiteSizedDungeon } from "../game/dungeonCatalog";
|
||||
import { dungeonById } from "../game/dungeonCatalog";
|
||||
import { dungeonEnabledForQueue } from "../game/dungeonQueuePolicy";
|
||||
import { loadDungeonQueuePolicy, useDungeonQueuePolicyStore } from "../app/dungeonQueuePolicyStore";
|
||||
import { RANDOMIZED_BITE_SIZED_CREATURE_CATALOG, requireDungeonDefinition } from "../game/dungeonRegistry";
|
||||
import { AVAILABLE_RANDOMIZED_CREATURE_CATALOG, BITE_SIZED_RANDOMIZED_CREATURE_CATALOG, requireDungeonDefinition } from "../game/dungeonRegistry";
|
||||
import { BrandMark, FrontSurface } from "./FrontSurface";
|
||||
import { DungeonDifficultySlider } from "./DungeonDifficultySlider";
|
||||
import { OnlineGroupPanel } from "./OnlineGroupPanel";
|
||||
@@ -23,9 +24,6 @@ import { ensureRandomizedBiteSizedRunContent } from "../app/randomizedBiteSizedC
|
||||
|
||||
installDungeonShellRuntime();
|
||||
|
||||
const DUNGEON = dungeonById(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID);
|
||||
const CANDIDATES = RANDOMIZED_BITE_SIZED_CREATURE_CATALOG;
|
||||
|
||||
export function RandomizedBiteSizedScreen({ onClose }: { readonly onClose: () => void }) {
|
||||
useContentManagerSnapshot();
|
||||
const activeCharacter = useShellStore((state) => state.activeCharacter);
|
||||
@@ -37,6 +35,7 @@ export function RandomizedBiteSizedScreen({ onClose }: { readonly onClose: () =>
|
||||
const updateOnlineMember = useOnlineGroupStore((state) => state.updateMember);
|
||||
const startOnlineActivity = useOnlineGroupStore((state) => state.startActivity);
|
||||
const [bossDifficulty, setBossDifficulty] = useState(1);
|
||||
const [pool, setPool] = useState<"bite-sized" | "all">("all");
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [readinessOpen, setReadinessOpen] = useState(false);
|
||||
@@ -48,7 +47,10 @@ export function RandomizedBiteSizedScreen({ onClose }: { readonly onClose: () =>
|
||||
const roles = character
|
||||
? character.categoryId === "rom" ? partyRolesForClass(character.classId) : specialization ? [specialization.role] : []
|
||||
: [];
|
||||
const installed = dungeonContentIsUsable(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID);
|
||||
const dungeonId = pool === "all" ? RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID : RANDOMIZED_BITE_SIZED_AVAILABLE_DUNGEON_ID;
|
||||
const DUNGEON = dungeonById(dungeonId);
|
||||
const CANDIDATES = pool === "all" ? AVAILABLE_RANDOMIZED_CREATURE_CATALOG : BITE_SIZED_RANDOMIZED_CREATURE_CATALOG;
|
||||
const installed = dungeonContentIsUsable(dungeonId);
|
||||
const mobCount = CANDIDATES.filter((candidate) => candidate.kind === "mob").length;
|
||||
const bossCount = CANDIDATES.filter((candidate) => candidate.kind === "boss").length;
|
||||
const queueEnabled = Boolean(DUNGEON && dungeonEnabledForQueue(queuePolicy, DUNGEON));
|
||||
@@ -56,8 +58,7 @@ export function RandomizedBiteSizedScreen({ onClose }: { readonly onClose: () =>
|
||||
const launch = async (role: PartyRole) => {
|
||||
if (!character || !DUNGEON?.available) return;
|
||||
const policy = await loadDungeonQueuePolicy();
|
||||
const selectedDungeon = randomRandomizedBiteSizedDungeon(undefined, Math.random,
|
||||
(candidate) => dungeonEnabledForQueue(policy, candidate));
|
||||
const selectedDungeon = DUNGEON && dungeonEnabledForQueue(policy, DUNGEON) ? DUNGEON : null;
|
||||
if (!selectedDungeon) {
|
||||
setError("No Randomized Bite-Sized location is enabled for this queue.");
|
||||
return;
|
||||
@@ -66,7 +67,7 @@ export function RandomizedBiteSizedScreen({ onClose }: { readonly onClose: () =>
|
||||
const runId = globalThis.crypto.randomUUID();
|
||||
setDownloading(true); setError("");
|
||||
try {
|
||||
await ensureRandomizedBiteSizedRunContent(runId);
|
||||
await ensureRandomizedBiteSizedRunContent(runId, undefined, selectedDungeon.id);
|
||||
enterDungeon(selectedDungeon.id, role, {
|
||||
difficultyId: requireDungeonDefinition(selectedDungeon.id).defaultDifficultyId,
|
||||
bossDifficulty,
|
||||
@@ -99,8 +100,8 @@ export function RandomizedBiteSizedScreen({ onClose }: { readonly onClose: () =>
|
||||
const install = async () => {
|
||||
setDownloading(true); setError("");
|
||||
try {
|
||||
await installContentPack(dungeonContentPackId(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID));
|
||||
if (!dungeonContentIsUsable(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID)) {
|
||||
await installContentPack(dungeonContentPackId(dungeonId));
|
||||
if (!dungeonContentIsUsable(dungeonId)) {
|
||||
throw new Error("The Stockades content pack could not be verified.");
|
||||
}
|
||||
} catch (caught) {
|
||||
@@ -123,7 +124,7 @@ export function RandomizedBiteSizedScreen({ onClose }: { readonly onClose: () =>
|
||||
<section className="randomized-bite-sized-screen__hero">
|
||||
<p className="front-kicker">A different lineup every run</p>
|
||||
<h1>Randomized Bite-Sized</h1>
|
||||
<p>The mode picks an eligible short layout—currently Cellblock Rush—deals a different cross-dungeon creature into every enemy slot, then rolls the boss separately.</p>
|
||||
<p>Cellblock Shuffle deals a different mob into every corridor slot and one final boss. Both come from the creature pool you choose.</p>
|
||||
<div className="randomized-bite-sized-screen__odds">
|
||||
<div><strong>{mobCount}</strong><span>mob types in the shuffle</span></div>
|
||||
<div><strong>{bossCount}</strong><span>possible final bosses</span></div>
|
||||
@@ -135,6 +136,11 @@ export function RandomizedBiteSizedScreen({ onClose }: { readonly onClose: () =>
|
||||
</section>
|
||||
<section className="randomized-bite-sized-screen__launch">
|
||||
<p className="front-kicker">Run setup</p>
|
||||
<div className="randomized-bite-sized-screen__roles randomized-bite-sized-screen__pool" role="group" aria-label="Randomized creature pool">
|
||||
<strong>Creature pool</strong>
|
||||
<button className="front-button" type="button" aria-pressed={pool === "bite-sized"} disabled={downloading} onClick={() => setPool("bite-sized")}>Available Bite-Sized dungeon sources</button>
|
||||
<button className="front-button" type="button" aria-pressed={pool === "all"} disabled={downloading} onClick={() => setPool("all")}>All available dungeon sources</button>
|
||||
</div>
|
||||
<h2>{queueEnabled ? DUNGEON?.title ?? "Cellblock Shuffle" : "No enabled location"}</h2>
|
||||
<DungeonDifficultySlider value={bossDifficulty} onChange={setBossDifficulty} selectedId="" select={() => undefined} />
|
||||
{!queueEnabled ? null : !installed ? (
|
||||
|
||||
@@ -54,4 +54,12 @@ describe("dungeon results and leaderboard", () => {
|
||||
expect(html).toContain(text);
|
||||
expect(html).toContain('aria-expanded="true"');
|
||||
});
|
||||
it("names the selected boss on a variable-boss dungeon board", () => {
|
||||
const html = renderToStaticMarkup(createElement(DungeonLeaderboard, { dungeonTitle: biteSized.title,
|
||||
data: { entries: [], total: 0 }, scope: "world", loading: false, filter: "final", onFilter: noop,
|
||||
expanded: new Set<string>(), onExpand: noop, offset: 0, onPage: noop, onRefresh: noop,
|
||||
difficultyLabel: "Normal", bossLabel: "Targorr the Dread", onBoss: noop, selectedId: "", select: noop }));
|
||||
expect(html).toContain("Boss: Targorr the Dread");
|
||||
expect(html).toContain("Change boss");
|
||||
});
|
||||
});
|
||||
@@ -3,13 +3,14 @@ import { fetchDungeonLeaderboard, localDungeonLeaderboard, subscribeDungeonLeade
|
||||
type LeaderboardScope } from "../app/dungeonLeaderboardClient";
|
||||
import type { DungeonClearFilter, DungeonLeaderboard } from "../game/dungeonRunTypes";
|
||||
|
||||
export function useDungeonLeaderboard(dungeonId: string, difficultyId: string, filter: DungeonClearFilter, bossDifficulty = 1) {
|
||||
export function useDungeonLeaderboard(dungeonId: string, difficultyId: string, filter: DungeonClearFilter,
|
||||
bossDifficulty = 1, bossId?: string) {
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [revision, setRevision] = useState(0);
|
||||
const [state, setState] = useState<{ data: DungeonLeaderboard; scope: LeaderboardScope; loading: boolean }>({
|
||||
data: { entries: [], total: 0 }, scope: "world", loading: true,
|
||||
});
|
||||
useEffect(() => { setOffset(0); }, [dungeonId, difficultyId, filter, bossDifficulty]);
|
||||
useEffect(() => { setOffset(0); }, [dungeonId, difficultyId, filter, bossDifficulty, bossId]);
|
||||
useEffect(() => subscribeDungeonLeaderboard(() => setRevision((value) => value + 1)), []);
|
||||
useEffect(() => {
|
||||
const refresh = () => setRevision((value) => value + 1);
|
||||
@@ -21,12 +22,12 @@ export function useDungeonLeaderboard(dungeonId: string, difficultyId: string, f
|
||||
let live = true;
|
||||
const timeout = setTimeout(() => controller.abort(), 8000);
|
||||
setState({ data: { entries: [], total: 0 }, scope: "world", loading: true });
|
||||
if (dungeonId) void fetchDungeonLeaderboard(dungeonId, difficultyId, filter, offset, controller.signal, bossDifficulty)
|
||||
if (dungeonId) void fetchDungeonLeaderboard(dungeonId, difficultyId, filter, offset, controller.signal, bossDifficulty, bossId)
|
||||
.then((data) => { if (live) setState({ data, scope: "world", loading: false }); })
|
||||
.catch(() => { if (live) setState({ data: localDungeonLeaderboard(dungeonId, difficultyId, filter, offset, bossDifficulty),
|
||||
.catch(() => { if (live) setState({ data: localDungeonLeaderboard(dungeonId, difficultyId, filter, offset, bossDifficulty, bossId),
|
||||
scope: "device", loading: false }); });
|
||||
else setState({ data: { entries: [], total: 0 }, scope: "world", loading: false });
|
||||
return () => { live = false; controller.abort(); clearTimeout(timeout); };
|
||||
}, [dungeonId, difficultyId, filter, offset, revision, bossDifficulty]);
|
||||
}, [dungeonId, difficultyId, filter, offset, revision, bossDifficulty, bossId]);
|
||||
return { ...state, offset, setOffset, refresh: () => setRevision((value) => value + 1) };
|
||||
}
|
||||
Reference in new issue
Block a user