Release Healer Man 0.1.39

This commit is contained in:
phenom committed 2026-09-13 11:07:56 -04:00
1 parent b417b7c3c3
commit ed0d2fea3d
51 files changed
+17811 -49

No files matched your search

+35 -14
View File
@@ -21,7 +21,10 @@ import {
import { useMenuController, type MenuAction } from "../input/useMenuController";
import { LazyLocalCompanionPanel } from "./LazyLocalCompanionPanel";
import { ControllerButton } from "./ControllerButton";
import { DungeonMapGraphic } from "./DungeonMapGraphic";
import { DungeonLeaderboard } from "./DungeonLeaderboard";
import { useDungeonLeaderboard } from "./useDungeonLeaderboard";
import type { DungeonClearFilter } from "../game/dungeonRunTypes";
import { DUNGEON_LEADERBOARD_PAGE_SIZE } from "../app/dungeonLeaderboardClient";
import { BrandMark, ControllerLegend, FrontSurface } from "./FrontSurface";
import { OnlineGroupPanel } from "./OnlineGroupPanel";
import { useOnlineGroupStore } from "../app/onlineGroupStore";
@@ -77,6 +80,20 @@ export function DungeonSelectScreen() {
?? firstCategoryDungeon
?? null;
const selectedDefinition = selectedDungeon ? dungeonDefinitionById(selectedDungeon.id) : null;
const [clearFilter, setClearFilter] = useState<DungeonClearFilter>("final");
const [difficultyId, setDifficultyId] = useState("");
const [expandedRuns, setExpandedRuns] = useState<ReadonlySet<string>>(new Set());
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);
const toggleRun = (id: string) => setExpandedRuns((current) => {
const next = new Set(current); if (next.has(id)) next.delete(id); else next.add(id); return next;
});
const cycleDifficulty = () => {
const variants = selectedDefinition?.difficultyVariants ?? [];
const index = variants.findIndex((variant) => variant.id === leaderboardDifficulty?.id);
if (variants.length) setDifficultyId(variants[(index + 1) % variants.length].id);
};
const selectedPackInstalled = selectedDungeon ? dungeonContentIsUsable(selectedDungeon.id) : false;
const canStart = Boolean(selectedDungeon?.available && selectedPackInstalled);
@@ -176,8 +193,17 @@ export function DungeonSelectScreen() {
id: `dungeon-${dungeon.id}`,
run: () => selectDungeon(dungeon.id),
enabled: true,
neighbors: index === 0 ? { up: `dungeon-category-${selectedCategory}` } : undefined,
neighbors: { ...(index === 0 ? { up: `dungeon-category-${selectedCategory}` } : {}), right: "leaderboard-final" },
})),
{ 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-refresh", run: leaderboard.refresh },
...leaderboard.data.entries.map((entry) => ({ id: `leaderboard-run-${entry.id}`, run: () => toggleRun(entry.id) })),
{ id: "leaderboard-prev", run: () => leaderboard.setOffset(Math.max(0, leaderboard.offset - DUNGEON_LEADERBOARD_PAGE_SIZE)),
enabled: !leaderboard.loading && leaderboard.offset > 0 },
{ id: "leaderboard-next", run: () => leaderboard.setOffset(leaderboard.offset + DUNGEON_LEADERBOARD_PAGE_SIZE),
enabled: !leaderboard.loading && leaderboard.offset + DUNGEON_LEADERBOARD_PAGE_SIZE < leaderboard.data.total },
...(rolePickerOpen
? availableRoles.map((role) => ({
id: `dungeon-role-${role}`,
@@ -193,7 +219,7 @@ export function DungeonSelectScreen() {
run: () => setRolePickerOpen(false),
}] : []),
{ id: "dungeon-back", run: returnToMainMenu },
], [availableRoles, categoryDungeons, downloading, firstCategoryDungeon, pendingDownload, returnToMainMenu, rolePickerOpen, selectedCategory, selectedDungeon]);
], [availableRoles, categoryDungeons, downloading, firstCategoryDungeon, pendingDownload, returnToMainMenu, rolePickerOpen, selectedCategory, selectedDungeon, selectedDefinition, leaderboard, expandedRuns, leaderboardDifficulty]);
const controller = useMenuController(actions, {
initialId: firstAvailable ? `dungeon-${firstAvailable.id}` : "dungeon-back",
onBack: pendingDownload
@@ -289,17 +315,12 @@ export function DungeonSelectScreen() {
<main className="dungeon-select-detail" aria-live="polite">
{selectedDungeon ? (
<>
<div className="dungeon-select-map">
{selectedDefinition ? (
<DungeonMapGraphic
dungeonId={selectedDefinition.id}
position={selectedDefinition.entrance.footPosition}
yaw={selectedDefinition.entrance.yaw}
/>
) : (
<div className="dungeon-select-map__placeholder" aria-hidden="true">HM</div>
)}
</div>
<DungeonLeaderboard dungeonTitle={selectedDungeon.title} data={leaderboard.data} scope={leaderboard.scope}
loading={leaderboard.loading} filter={clearFilter} onFilter={setClearFilter}
expanded={expandedRuns} onExpand={toggleRun} offset={leaderboard.offset} onPage={leaderboard.setOffset}
onRefresh={leaderboard.refresh} difficultyLabel={leaderboardDifficulty?.difficulty === "heroic" ? "Heroic" : "Normal"}
onDifficulty={(selectedDefinition?.difficultyVariants.length ?? 0) > 1 ? cycleDifficulty : undefined}
selectedId={controller.selectedId} select={controller.select} />
<section className="dungeon-select-copy">
<p className="front-kicker">{canStart ? "Playable expedition" : selectedDungeon.available ? "Content required" : "In development"}</p>
<h2>{selectedDungeon.title}</h2>