Release Healer Man 0.1.22
This commit is contained in:
1 parent
f6f2375145
commit
7f6001723a
144 files changed
+54107
-690
No files matched your search
+116
-16
@@ -1,5 +1,5 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { contentPackIsUsable } from "../content/contentManager";
|
||||
import { dungeonContentIsUsable, installContentPack } from "../content/contentManager";
|
||||
import { useContentManagerSnapshot } from "../content/useContentManager";
|
||||
import { classById, raceById } from "../app/characterCatalog";
|
||||
import { installDungeonShellRuntime } from "../app/dungeonShellRuntime";
|
||||
@@ -44,8 +44,13 @@ const ROLE_DETAILS: Readonly<Record<PartyRole, { readonly sigil: string; readonl
|
||||
},
|
||||
};
|
||||
|
||||
interface PendingDungeonDownload {
|
||||
readonly dungeonId: DungeonId;
|
||||
readonly role: PartyRole | null;
|
||||
}
|
||||
|
||||
export function DungeonSelectScreen() {
|
||||
useContentManagerSnapshot();
|
||||
const contentState = useContentManagerSnapshot();
|
||||
const activeCharacter = useShellStore((state) => state.activeCharacter);
|
||||
const rosterCharacter = useShellStore(selectedCharacter);
|
||||
const enterDungeon = useShellStore((state) => state.enterDungeon);
|
||||
@@ -60,12 +65,17 @@ export function DungeonSelectScreen() {
|
||||
const [selectedCategory, setSelectedCategory] = useState<DungeonCategoryId>(firstAvailable?.category ?? "wow");
|
||||
const [selectedDungeonId, setSelectedDungeonId] = useState<DungeonId | "">(firstAvailable?.id ?? "");
|
||||
const [rolePickerOpen, setRolePickerOpen] = useState(false);
|
||||
const [pendingDownload, setPendingDownload] = useState<PendingDungeonDownload | null>(null);
|
||||
const [downloadError, setDownloadError] = useState("");
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const categoryDungeons = dungeonsByCategory(selectedCategory);
|
||||
const firstCategoryDungeon = categoryDungeons.find((dungeon) => dungeon.available) ?? categoryDungeons[0];
|
||||
const selectedDungeon = categoryDungeons.find((dungeon) => dungeon.id === selectedDungeonId)
|
||||
?? firstCategoryDungeon
|
||||
?? null;
|
||||
const selectedDefinition = selectedDungeon ? dungeonDefinitionById(selectedDungeon.id) : null;
|
||||
const selectedPackInstalled = selectedDungeon ? dungeonContentIsUsable(selectedDungeon.id) : false;
|
||||
const canStart = Boolean(selectedDungeon?.available && selectedPackInstalled);
|
||||
|
||||
const savedSpecialization = specializationById(character?.specializationId);
|
||||
const availableRoles = character
|
||||
@@ -74,6 +84,11 @@ export function DungeonSelectScreen() {
|
||||
const selectDungeon = (dungeonId: DungeonId) => {
|
||||
setSelectedDungeonId(dungeonId);
|
||||
setRolePickerOpen(false);
|
||||
setDownloadError("");
|
||||
const dungeon = DUNGEON_CATALOG.find((candidate) => candidate.id === dungeonId);
|
||||
if (dungeon?.available && !dungeonContentIsUsable(dungeonId)) {
|
||||
setPendingDownload({ dungeonId, role: null });
|
||||
}
|
||||
};
|
||||
const selectCategory = (category: DungeonCategoryId) => {
|
||||
const dungeons = dungeonsByCategory(category);
|
||||
@@ -83,7 +98,7 @@ export function DungeonSelectScreen() {
|
||||
setRolePickerOpen(false);
|
||||
};
|
||||
|
||||
const enterAsRole = async (role: PartyRole) => {
|
||||
const continueAsRole = async (role: PartyRole) => {
|
||||
if (!selectedDungeon?.available || !character) return;
|
||||
if (!onlineGroup || !session || session.kind !== "account") {
|
||||
enterDungeon(selectedDungeon.id, role);
|
||||
@@ -100,7 +115,51 @@ export function DungeonSelectScreen() {
|
||||
partySize: 5,
|
||||
});
|
||||
};
|
||||
const actions = useMemo<MenuAction[]>(() => [
|
||||
|
||||
const requestRolePicker = () => {
|
||||
if (!selectedDungeon?.available) return;
|
||||
if (!dungeonContentIsUsable(selectedDungeon.id)) {
|
||||
setDownloadError("");
|
||||
setPendingDownload({ dungeonId: selectedDungeon.id, role: null });
|
||||
return;
|
||||
}
|
||||
setRolePickerOpen(true);
|
||||
};
|
||||
|
||||
const enterAsRole = async (role: PartyRole) => {
|
||||
if (!selectedDungeon?.available || !character) return;
|
||||
if (!dungeonContentIsUsable(selectedDungeon.id)) {
|
||||
setDownloadError("");
|
||||
setPendingDownload({ dungeonId: selectedDungeon.id, role });
|
||||
return;
|
||||
}
|
||||
await continueAsRole(role);
|
||||
};
|
||||
|
||||
const confirmDownload = async () => {
|
||||
if (!pendingDownload || downloading) return;
|
||||
setDownloading(true);
|
||||
setDownloadError("");
|
||||
try {
|
||||
await installContentPack(`dungeon:${pendingDownload.dungeonId}`);
|
||||
if (!dungeonContentIsUsable(pendingDownload.dungeonId)) {
|
||||
throw new Error("The downloaded expedition could not be verified on this device.");
|
||||
}
|
||||
const role = pendingDownload.role;
|
||||
setPendingDownload(null);
|
||||
if (role) await continueAsRole(role);
|
||||
else setRolePickerOpen(true);
|
||||
} catch (error) {
|
||||
setDownloadError(error instanceof Error ? error.message : "The expedition download failed.");
|
||||
} finally {
|
||||
setDownloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const actions = useMemo<MenuAction[]>(() => pendingDownload ? [
|
||||
{ id: "dungeon-download-cancel", run: () => setPendingDownload(null), enabled: !downloading },
|
||||
{ id: "dungeon-download-confirm", run: () => void confirmDownload(), enabled: !downloading },
|
||||
] : [
|
||||
...DUNGEON_CATEGORIES.map((category, index) => ({
|
||||
id: `dungeon-category-${category.id}`,
|
||||
run: () => selectCategory(category.id),
|
||||
@@ -123,7 +182,7 @@ export function DungeonSelectScreen() {
|
||||
}))
|
||||
: [{
|
||||
id: "choose-dungeon-role",
|
||||
run: () => setRolePickerOpen(true),
|
||||
run: requestRolePicker,
|
||||
enabled: Boolean(selectedDungeon?.available),
|
||||
}]),
|
||||
...(rolePickerOpen ? [{
|
||||
@@ -131,17 +190,16 @@ export function DungeonSelectScreen() {
|
||||
run: () => setRolePickerOpen(false),
|
||||
}] : []),
|
||||
{ id: "dungeon-back", run: returnToMainMenu },
|
||||
], [availableRoles, categoryDungeons, enterDungeon, firstCategoryDungeon, returnToMainMenu, rolePickerOpen, selectedCategory, selectedDungeon]);
|
||||
], [availableRoles, categoryDungeons, downloading, firstCategoryDungeon, pendingDownload, returnToMainMenu, rolePickerOpen, selectedCategory, selectedDungeon]);
|
||||
const controller = useMenuController(actions, {
|
||||
initialId: firstAvailable ? `dungeon-${firstAvailable.id}` : "dungeon-back",
|
||||
onBack: rolePickerOpen ? () => setRolePickerOpen(false) : returnToMainMenu,
|
||||
onBack: pendingDownload
|
||||
? () => { if (!downloading) setPendingDownload(null); }
|
||||
: rolePickerOpen ? () => setRolePickerOpen(false) : returnToMainMenu,
|
||||
});
|
||||
|
||||
const race = character ? raceById(character.raceId) : null;
|
||||
const characterClass = character ? classById(character.classId) : null;
|
||||
const selectedPackInstalled = selectedDungeon ? contentPackIsUsable(`dungeon:${selectedDungeon.id}`) : false;
|
||||
const canStart = Boolean(selectedDungeon?.available && selectedPackInstalled);
|
||||
|
||||
return (
|
||||
<DualDisplayFrame
|
||||
contextLabel="Dungeon details"
|
||||
@@ -203,7 +261,7 @@ export function DungeonSelectScreen() {
|
||||
<b aria-hidden="true">{String(index + 1).padStart(2, "0")}</b>
|
||||
<span><strong>{dungeon.title}</strong><small>{dungeon.entranceName}</small></span>
|
||||
<em>{dungeon.available
|
||||
? contentPackIsUsable(`dungeon:${dungeon.id}`) ? "Ready" : "Download required"
|
||||
? dungeonContentIsUsable(dungeon.id) ? "Ready" : "Download required"
|
||||
: "Coming soon"}</em>
|
||||
</ControllerButton>
|
||||
))}
|
||||
@@ -242,7 +300,7 @@ export function DungeonSelectScreen() {
|
||||
<p className="front-notice" role="status">{selectedDungeon.availabilityReason}</p>
|
||||
)}
|
||||
{selectedDungeon.available && !selectedPackInstalled && (
|
||||
<p className="front-notice" role="status">Install this expedition from Main menu / Downloads before playing offline.</p>
|
||||
<p className="front-notice" role="status">Select this expedition to download its required files.</p>
|
||||
)}
|
||||
<OnlineGroupPanel />
|
||||
{rolePickerOpen && characterClass ? (
|
||||
@@ -298,11 +356,11 @@ export function DungeonSelectScreen() {
|
||||
select={controller.select}
|
||||
className="front-button front-button--primary dungeon-select-enter"
|
||||
type="button"
|
||||
disabled={!canStart}
|
||||
onClick={() => setRolePickerOpen(true)}
|
||||
disabled={!selectedDungeon.available}
|
||||
onClick={requestRolePicker}
|
||||
>
|
||||
<span>Choose role</span>
|
||||
<small>{canStart ? "Select your role before entering" : selectedDungeon.available ? "Install this expedition from Downloads" : "This expedition is not ready yet"}</small>
|
||||
<span>{canStart ? "Choose role" : selectedDungeon.available ? "Download and choose role" : "Unavailable"}</span>
|
||||
<small>{canStart ? "Select your role before entering" : selectedDungeon.available ? "Required files will be verified before play" : "This expedition is not ready yet"}</small>
|
||||
</ControllerButton>
|
||||
</>
|
||||
)}
|
||||
@@ -318,6 +376,48 @@ export function DungeonSelectScreen() {
|
||||
</main>
|
||||
|
||||
<ControllerLegend back />
|
||||
{pendingDownload && (
|
||||
<div className="front-modal-backdrop">
|
||||
<section className="front-confirm" role="alertdialog" aria-modal="true" aria-labelledby="dungeon-download-title">
|
||||
<p className="front-kicker">Download required</p>
|
||||
<h2 id="dungeon-download-title">
|
||||
Download {DUNGEON_CATALOG.find((dungeon) => dungeon.id === pendingDownload.dungeonId)?.title ?? "this expedition"}?
|
||||
</h2>
|
||||
<p>The map, creatures, and shared dependencies are not available on this device. Download and verify them now, then continue?</p>
|
||||
{downloading && (
|
||||
<p role="status">
|
||||
Downloading and verifying files
|
||||
{contentState.totalBytes > 0 ? ` — ${Math.round((contentState.downloadedBytes / contentState.totalBytes) * 100)}%` : "..."}
|
||||
</p>
|
||||
)}
|
||||
{downloadError && <p className="front-notice" role="alert">{downloadError}</p>}
|
||||
<div>
|
||||
<ControllerButton
|
||||
controlId="dungeon-download-cancel"
|
||||
selectedId={controller.selectedId}
|
||||
select={controller.select}
|
||||
className="front-button"
|
||||
type="button"
|
||||
disabled={downloading}
|
||||
onClick={() => setPendingDownload(null)}
|
||||
>
|
||||
Not now
|
||||
</ControllerButton>
|
||||
<ControllerButton
|
||||
controlId="dungeon-download-confirm"
|
||||
selectedId={controller.selectedId}
|
||||
select={controller.select}
|
||||
className="front-button front-button--primary"
|
||||
type="button"
|
||||
disabled={downloading}
|
||||
onClick={() => void confirmDownload()}
|
||||
>
|
||||
{downloading ? "Downloading..." : downloadError ? "Try again" : "Download and continue"}
|
||||
</ControllerButton>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</FrontSurface>
|
||||
}
|
||||
bottom={<LazyLocalCompanionPanel />}
|
||||
|
||||
Reference in new issue
Block a user