Update Healer Man server
This commit is contained in:
1 parent
8b9c95b24f
commit
14e105642c
13 files changed
+355
-12
No files matched your search
@@ -1,7 +1,10 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useShellStore } from "../app/shellStore";
|
||||
import { useOnlineGroupStore } from "../app/onlineGroupStore";
|
||||
import { awaitSharedDungeonRunPlacement, localDungeonRunPlacement, saveLocalDungeonRun, submitDungeonRun, type LeaderboardScope } from "../app/dungeonLeaderboardClient";
|
||||
import { useOnlineSessionStore } from "../app/onlineSessionStore";
|
||||
import { startAnotherBiteSizedDungeon } from "../app/biteSizedDungeonRuntime";
|
||||
import { dungeonById } from "../game/dungeonCatalog";
|
||||
import { completedDungeonRun, useDungeonRunStore } from "../game/dungeonRunStore";
|
||||
import { formatDungeonDuration, type DungeonRunPlacement, type DungeonRunProgress } from "../game/dungeonRunTypes";
|
||||
import catalog from "../game/generated/dungeonLeaderboardCatalog.generated.json";
|
||||
@@ -18,20 +21,25 @@ export interface RunPlacementState {
|
||||
readonly loading: boolean;
|
||||
}
|
||||
|
||||
export function DungeonRunResultsDialog({ progress, ranking, onFinish, onLoot, onRetry }: {
|
||||
export function DungeonRunResultsDialog({ progress, ranking, onFinish, onLoot, onRetry, onAnother,
|
||||
startingAnother = false, anotherMessage }: {
|
||||
readonly progress: DungeonRunProgress;
|
||||
readonly ranking: RunPlacementState;
|
||||
readonly onFinish: () => void;
|
||||
readonly onLoot: () => void;
|
||||
readonly onRetry: () => void;
|
||||
readonly onAnother?: () => void;
|
||||
readonly startingAnother?: boolean;
|
||||
readonly anotherMessage?: string;
|
||||
}) {
|
||||
const definition = catalog.dungeons.find((dungeon) => dungeon.id === progress.dungeonId);
|
||||
const difficulty = definition?.difficulties.find((entry) => entry.id === progress.difficultyId);
|
||||
const fullClear = difficulty ? isLeaderboardFullClear(difficulty, progress.killedBossIds) : false;
|
||||
const controller = useMenuController([
|
||||
...(onAnother ? [{ id: "run-another", run: onAnother, enabled: !startingAnother }] : []),
|
||||
{ id: "run-loot", run: onLoot }, { id: "run-finish", run: onFinish },
|
||||
...(ranking.scope === "device" ? [{ id: "run-retry", run: onRetry }] : []),
|
||||
], { initialId: "run-loot", onBack: onLoot });
|
||||
], { initialId: onAnother ? "run-another" : "run-loot", onBack: onLoot });
|
||||
const controls = { selectedId: controller.selectedId, select: controller.select };
|
||||
const panel = useRef<HTMLElement>(null);
|
||||
useEffect(() => {
|
||||
@@ -64,11 +72,16 @@ export function DungeonRunResultsDialog({ progress, ranking, onFinish, onLoot, o
|
||||
{ranking.placement?.fullRank ? <p className="dungeon-results__full-rank">Full dungeon clears: <strong>#{ranking.placement.fullRank}</strong> of {ranking.placement.fullTotal}</p> : null}
|
||||
{!progress.timed ? <p className="dungeon-results__note">This resumed run has no saved timer. Start a new expedition to record a leaderboard time.</p> : null}
|
||||
{ranking.scope === "device" ? <p className="dungeon-results__note">Your time is saved on this device. Reconnect to submit it to the world leaderboard.</p> : null}
|
||||
{anotherMessage ? <p className="dungeon-results__note" role="status">{anotherMessage}</p> : null}
|
||||
<div className="dungeon-results__details"><DungeonRunDetails dungeonId={progress.dungeonId}
|
||||
difficultyId={progress.difficultyId} bossDifficulty={progress.bossDifficulty} party={progress.party} killedBossIds={progress.killedBossIds} /></div>
|
||||
<footer>
|
||||
{onAnother ? <ControllerButton {...controls} controlId="run-another" className="button button--primary"
|
||||
disabled={startingAnother} onClick={onAnother}>
|
||||
{startingAnother ? "Starting next dungeon…" : "Another random Bite-Sized dungeon"}
|
||||
</ControllerButton> : null}
|
||||
<ControllerButton {...controls} controlId="run-loot" className="button" onClick={onLoot}>Stay and loot</ControllerButton>
|
||||
<ControllerButton {...controls} controlId="run-finish" className="button button--primary" onClick={onFinish}>Return to main menu</ControllerButton>
|
||||
<ControllerButton {...controls} controlId="run-finish" className={`button ${onAnother ? "" : "button--primary"}`} onClick={onFinish}>Return to main menu</ControllerButton>
|
||||
{ranking.scope === "device" ? <ControllerButton {...controls} controlId="run-retry" className="button" onClick={onRetry}>Retry submission</ControllerButton> : null}
|
||||
</footer>
|
||||
</section>
|
||||
@@ -80,6 +93,12 @@ export function DungeonRunResults() {
|
||||
const overlay = useGameStore((state) => state.overlay);
|
||||
const record = useMemo(() => progress ? completedDungeonRun(progress) : null, [progress]);
|
||||
const [retry, setRetry] = useState(0);
|
||||
const [startingAnother, setStartingAnother] = useState(false);
|
||||
const [anotherError, setAnotherError] = useState("");
|
||||
const group = useOnlineGroupStore((state) => state.snapshot.group);
|
||||
const groupBusy = useOnlineGroupStore((state) => state.busy);
|
||||
const sessionOwnerId = useShellStore((state) => state.session?.ownerId ?? null);
|
||||
const online = useOnlineSessionStore((state) => state.snapshot);
|
||||
const [ranking, setRanking] = useState<RunPlacementState>({ scope: "world", placement: null, loading: false });
|
||||
useEffect(() => {
|
||||
if (!record) { setRanking({ scope: "world", placement: null, loading: false }); return; }
|
||||
@@ -95,7 +114,22 @@ export function DungeonRunResults() {
|
||||
return () => { live = false; };
|
||||
}, [record, retry]);
|
||||
if (overlay !== "dungeon-results" || !progress || progress.finishedAt === null) return null;
|
||||
const biteSized = dungeonById(progress.dungeonId)?.category === "bite-sized";
|
||||
const partyLeader = group ? group.leaderAccountId === sessionOwnerId : !online || online.authority;
|
||||
const canStartAnother = biteSized && partyLeader;
|
||||
const leaderMessage = biteSized && !partyLeader
|
||||
? "The party leader can start the next random Bite-Sized dungeon."
|
||||
: "";
|
||||
return <DungeonRunResultsDialog progress={progress} ranking={ranking} onRetry={() => setRetry((value) => value + 1)}
|
||||
onAnother={canStartAnother ? () => {
|
||||
setStartingAnother(true);
|
||||
setAnotherError("");
|
||||
void startAnotherBiteSizedDungeon(progress).then((started) => {
|
||||
if (!started) setAnotherError(useShellStore.getState().notice || "The next dungeon could not be started.");
|
||||
}).finally(() => setStartingAnother(false));
|
||||
} : undefined}
|
||||
startingAnother={startingAnother || groupBusy}
|
||||
anotherMessage={anotherError || leaderMessage}
|
||||
onLoot={() => useGameStore.getState().closeOverlay()} onFinish={() => {
|
||||
useShellStore.getState().saveActiveDungeonRunCheckpoint(null);
|
||||
useGameStore.getState().closeOverlay();
|
||||
|
||||
@@ -7,6 +7,7 @@ import catalog from "../game/generated/dungeonLeaderboardCatalog.generated.json"
|
||||
import type { DungeonRunProgress } from "../game/dungeonRunTypes";
|
||||
|
||||
const caves = catalog.dungeons.find((entry) => entry.id === "barren-caves")!;
|
||||
const biteSized = catalog.dungeons.find((entry) => entry.id === "bite-sized-stormwind-stockade")!;
|
||||
const progress: DungeonRunProgress = { id: "ui-caves-test", dungeonId: caves.id, difficultyId: "normal", timed: true,
|
||||
startedAt: 1000, finishedAt: 13345, durationMs: 12345, killedBossIds: [caves.finalBossId],
|
||||
party: [{ name: "Tester", classId: "rom-priest", level: 3, role: "healer", npc: false },
|
||||
@@ -27,6 +28,15 @@ describe("dungeon results and leaderboard", () => {
|
||||
onFinish: noop, onLoot: noop, onRetry: noop }));
|
||||
expect(html).toContain("This device leaderboard"); expect(html).toContain("Retry submission");
|
||||
});
|
||||
it("offers the party leader another random bite-sized run without removing the menu exit", () => {
|
||||
const biteProgress = { ...progress, id: "bite-sized-ui-test", dungeonId: biteSized.id,
|
||||
killedBossIds: [biteSized.finalBossId] };
|
||||
const html = renderToStaticMarkup(createElement(DungeonRunResultsDialog, { progress: biteProgress,
|
||||
ranking: { scope: "world", loading: false, placement: null },
|
||||
onFinish: noop, onLoot: noop, onRetry: noop, onAnother: noop }));
|
||||
expect(html).toContain("Another random Bite-Sized dungeon");
|
||||
expect(html).toContain("Return to main menu");
|
||||
});
|
||||
it("offers both clear filters and expandable party/boss records in the dungeon picker", () => {
|
||||
const html = renderToStaticMarkup(createElement(DungeonLeaderboard, { dungeonTitle: caves.title,
|
||||
data: { entries: [{ ...progress, durationMs: progress.durationMs!, finishedAt: progress.finishedAt!, fullClear: true, rank: 1 }], total: 1 },
|
||||
|
||||
Reference in new issue
Block a user