548 lines
25 KiB
TypeScript
548 lines
25 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
|
import { classById, raceById } from "../app/characterCatalog";
|
|
import { installManastormShellRuntime } from "../app/manastormShellRuntime";
|
|
import { selectedCharacter, useShellStore } from "../app/shellStore";
|
|
import { preloadAvatarModels } from "../avatar/avatarPreloader";
|
|
import { DualDisplayFrame } from "../components/DualDisplayFrame";
|
|
import { contentPackIsUsable, installContentPack } from "../content/contentManager";
|
|
import { useContentManagerSnapshot } from "../content/useContentManager";
|
|
import { MANASTORM_CATALOG_SUMMARY } from "../game/manastormCatalogSummary";
|
|
import {
|
|
GENERATED_MANASTORM_STAGE_ASSET_PACKAGE_COUNT,
|
|
generatedManastormContentPackId,
|
|
} from "../game/manastormAssetRegistry";
|
|
import { INSTALLED_MANASTORM_RUNTIME_CATALOG } from "../game/manastormInstalledCatalog";
|
|
import { effectiveManastormCatalog } from "../game/manastormAdminConfig";
|
|
import { useManastormAdminStore } from "../game/manastormAdminStore";
|
|
import { manastormActiveSpellLoadoutModel } from "../game/manastormSpellLoadout";
|
|
import {
|
|
drawManastormStage,
|
|
manastormCacheChance,
|
|
manastormModeForCharacterLevel,
|
|
manastormReward,
|
|
} from "../game/manastorm";
|
|
import {
|
|
MANASTORM_PARTY_SIZES,
|
|
createEmptyManastormProgress,
|
|
manastormModeProgress,
|
|
manastormPartyProgress,
|
|
type ManastormPartySize,
|
|
} from "../game/manastormProgress";
|
|
import {
|
|
createDungeonParty,
|
|
partyRolesForClass,
|
|
PARTY_ROLE_LABELS,
|
|
type PartyRole,
|
|
} from "../game/partyStore";
|
|
import { useMenuController, type MenuAction } from "../input/useMenuController";
|
|
import { LazyLocalCompanionPanel } from "./LazyLocalCompanionPanel";
|
|
import { ControllerButton } from "./ControllerButton";
|
|
import { AbilityTooltipTarget } from "./AbilityTooltip";
|
|
import { BrandMark, ControllerLegend, FrontSurface } from "./FrontSurface";
|
|
import { OnlineGroupPanel } from "./OnlineGroupPanel";
|
|
import { useOnlineGroupStore } from "../app/onlineGroupStore";
|
|
import { specializationById } from "../game/specializationCatalog";
|
|
import { useCompanionPartyPreferences } from "../game/companionPartyPreferences";
|
|
import { useCompanionSpecReadinessStore } from "../game/companionSpecReadinessStore";
|
|
import { CompanionPoolControls } from "./CompanionPoolControls";
|
|
import { CompanionReadinessScreen } from "./CompanionReadinessScreen";
|
|
|
|
installManastormShellRuntime();
|
|
|
|
const ROLE_DETAILS: Readonly<Record<PartyRole, { readonly sigil: string; readonly summary: string }>> = {
|
|
tank: { sigil: "T", summary: "Hold each empowered boss while your healer and damage party work." },
|
|
healer: { sigil: "+", summary: "Keep the expedition alive as every new storm hits harder." },
|
|
damage: { sigil: "D", summary: "Burn through guardians and bosses with a balanced party." },
|
|
};
|
|
|
|
export function ManastormSelectScreen() {
|
|
const contentState = useContentManagerSnapshot();
|
|
const activeCharacter = useShellStore((state) => state.activeCharacter);
|
|
const rosterCharacter = useShellStore(selectedCharacter);
|
|
const enterManastorm = useShellStore((state) => state.enterManastorm);
|
|
const saveSelectedManastormLoadout = useShellStore((state) => state.saveSelectedManastormLoadout);
|
|
const returnToMainMenu = useShellStore((state) => state.returnToMainMenu);
|
|
const notice = useShellStore((state) => state.notice);
|
|
const session = useShellStore((state) => state.session);
|
|
const onlineGroup = useOnlineGroupStore((state) => state.snapshot.group);
|
|
const fillWithAi = useOnlineGroupStore((state) => state.fillWithAi);
|
|
const updateOnlineMember = useOnlineGroupStore((state) => state.updateMember);
|
|
const startOnlineActivity = useOnlineGroupStore((state) => state.startActivity);
|
|
const adminConfig = useManastormAdminStore((state) => state.config);
|
|
const character = activeCharacter ?? rosterCharacter;
|
|
const savedSpecialization = specializationById(character?.specializationId);
|
|
const availableRoles = character
|
|
? character.categoryId === "rom" ? partyRolesForClass(character.classId) : savedSpecialization ? [savedSpecialization.role] : []
|
|
: [];
|
|
const [partySize, setPartySize] = useState<ManastormPartySize>(5);
|
|
const includeBetaSpecs = useCompanionPartyPreferences((state) => state.includeBetaSpecs);
|
|
const readinessPolicy = useCompanionSpecReadinessStore((state) => state.config);
|
|
const readinessPolicyRevision = useCompanionSpecReadinessStore((state) => state.revision);
|
|
const groupHumanCount = onlineGroup?.members.length ?? 1;
|
|
const effectivePartySize = onlineGroup
|
|
? (fillWithAi ? Math.max(partySize, groupHumanCount) : groupHumanCount) as ManastormPartySize
|
|
: partySize;
|
|
const [resume, setResume] = useState(true);
|
|
const progress = character?.manastormProgress ?? createEmptyManastormProgress();
|
|
const effectiveCatalog = useMemo(
|
|
() => effectiveManastormCatalog(adminConfig, INSTALLED_MANASTORM_RUNTIME_CATALOG),
|
|
[adminConfig],
|
|
);
|
|
const modeId = manastormModeForCharacterLevel(
|
|
character?.level ?? 1,
|
|
effectiveCatalog,
|
|
);
|
|
const modeEnabled = adminConfig.global.enabled
|
|
&& (modeId === 2 ? adminConfig.global.endgameEnabled : adminConfig.global.levelingEnabled);
|
|
const eligibleStages = useMemo(() => effectiveCatalog.stages?.filter((stage) => (
|
|
!stage.modeIds?.length || stage.modeIds.includes(modeId)
|
|
)) ?? [], [effectiveCatalog, modeId]);
|
|
const hasReadyBosses = eligibleStages.length > 0;
|
|
const requiredContentPackIds = useMemo(() => Object.freeze([...new Set(eligibleStages.flatMap((stage) => {
|
|
if (stage.assetPackageId) {
|
|
const packId = generatedManastormContentPackId(stage.assetPackageId);
|
|
return packId ? [packId] : [];
|
|
}
|
|
return stage.dungeonId ? [`dungeon:${stage.dungeonId}`] : [];
|
|
}))].sort()), [eligibleStages]);
|
|
const missingContentPackIds = import.meta.env.DEV
|
|
? []
|
|
: requiredContentPackIds.filter((packId) => !contentPackIsUsable(packId));
|
|
const canEnter = modeEnabled && hasReadyBosses;
|
|
const modeProgress = manastormModeProgress(progress, modeId);
|
|
const modeLabel = modeId === 2 ? "End-game" : "Leveling";
|
|
const partyProgress = manastormPartyProgress(progress, effectivePartySize, modeId);
|
|
const checkpoint = Math.max(1, partyProgress.checkpointLevel);
|
|
const startingLevel = resume && checkpoint > 1 ? checkpoint : 1;
|
|
const cacheChance = modeId === 2
|
|
? Math.round(manastormCacheChance(startingLevel, modeProgress.cacheMeter) * 100)
|
|
: 100;
|
|
const rewardPreview = useMemo(() => {
|
|
try {
|
|
const preview = drawManastormStage(
|
|
startingLevel,
|
|
effectivePartySize,
|
|
[],
|
|
null,
|
|
() => 0.5,
|
|
effectiveCatalog,
|
|
modeId,
|
|
Math.max(startingLevel, partyProgress.highestLevel + 1),
|
|
);
|
|
return manastormReward(
|
|
preview.encounter,
|
|
startingLevel,
|
|
effectivePartySize,
|
|
effectiveCatalog,
|
|
modeId,
|
|
);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}, [effectiveCatalog, effectivePartySize, modeId, partyProgress.highestLevel, startingLevel]);
|
|
const spellLoadoutModel = useMemo(() => character
|
|
? manastormActiveSpellLoadoutModel(character.classId, character.level, character.talentRanks)
|
|
: null, [character?.classId, character?.level, character?.talentRanks]);
|
|
const availableSpellIds = useMemo(
|
|
() => new Set(spellLoadoutModel?.options.map((option) => option.id) ?? []),
|
|
[spellLoadoutModel],
|
|
);
|
|
const persistedSpellIds = progress.loadout.filter((id) => availableSpellIds.has(id));
|
|
const [selectedSpellIds, setSelectedSpellIds] = useState<readonly string[]>(persistedSpellIds);
|
|
const [pendingDownloadRole, setPendingDownloadRole] = useState<PartyRole | null>(null);
|
|
const [downloading, setDownloading] = useState(false);
|
|
const [downloadError, setDownloadError] = useState("");
|
|
const [readinessOpen, setReadinessOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setSelectedSpellIds(persistedSpellIds);
|
|
}, [progress.loadout.join("|"), spellLoadoutModel?.classId, spellLoadoutModel?.characterLevel]);
|
|
|
|
const toggleSpell = (spellId: string) => {
|
|
if (!spellLoadoutModel) return;
|
|
const selected = selectedSpellIds.includes(spellId);
|
|
const next = selected
|
|
? selectedSpellIds.filter((id) => id !== spellId)
|
|
: selectedSpellIds.length < spellLoadoutModel.maxSelections
|
|
? [...selectedSpellIds, spellId]
|
|
: selectedSpellIds;
|
|
if (next === selectedSpellIds) return;
|
|
if (saveSelectedManastormLoadout(next)) setSelectedSpellIds(next);
|
|
};
|
|
|
|
const preloadRole = (role: PartyRole) => {
|
|
if (!character) return;
|
|
const party = createDungeonParty(
|
|
character.id,
|
|
character.level,
|
|
role,
|
|
effectivePartySize,
|
|
character.categoryId ?? "wow",
|
|
{
|
|
readinessPolicy,
|
|
readinessPolicyRevision,
|
|
includeBetaSpecs,
|
|
excludedPlayerSpecializationId: character.specializationId,
|
|
},
|
|
);
|
|
void preloadAvatarModels([character, ...party]).catch((error) => {
|
|
console.warn("Selected party avatar preloading could not be completed", error);
|
|
});
|
|
};
|
|
|
|
const continueAsRole = async (role: PartyRole) => {
|
|
if (!canEnter || !character) return;
|
|
if (!onlineGroup || !session || session.kind !== "account") {
|
|
enterManastorm(role, partySize, startingLevel);
|
|
return;
|
|
}
|
|
if (!await updateOnlineMember(session, character, role)) return;
|
|
if (onlineGroup.leaderAccountId !== session.ownerId) {
|
|
useShellStore.getState().setNotice(`Ready as ${PARTY_ROLE_LABELS[role]}. Waiting for the group leader to start.`);
|
|
return;
|
|
}
|
|
await startOnlineActivity(session, {
|
|
type: "manastorm",
|
|
contentId: "manastorm",
|
|
partySize: effectivePartySize,
|
|
startingLevel,
|
|
includeBetaSpecs,
|
|
});
|
|
};
|
|
|
|
const enterAsRole = async (role: PartyRole) => {
|
|
if (!canEnter || !character) return;
|
|
if (missingContentPackIds.length > 0) {
|
|
setDownloadError("");
|
|
setPendingDownloadRole(role);
|
|
return;
|
|
}
|
|
await continueAsRole(role);
|
|
};
|
|
|
|
const confirmDownload = async () => {
|
|
if (!pendingDownloadRole || downloading) return;
|
|
setDownloading(true);
|
|
setDownloadError("");
|
|
try {
|
|
for (const packId of requiredContentPackIds) {
|
|
if (!contentPackIsUsable(packId)) await installContentPack(packId);
|
|
}
|
|
const stillMissing = requiredContentPackIds.filter((packId) => !contentPackIsUsable(packId));
|
|
if (stillMissing.length > 0) {
|
|
throw new Error("One or more downloaded Manastorm maps could not be verified on this device.");
|
|
}
|
|
const role = pendingDownloadRole;
|
|
setPendingDownloadRole(null);
|
|
await continueAsRole(role);
|
|
} catch (error) {
|
|
setDownloadError(error instanceof Error ? error.message : "The Manastorm download failed.");
|
|
} finally {
|
|
setDownloading(false);
|
|
}
|
|
};
|
|
|
|
const actions = useMemo<MenuAction[]>(() => pendingDownloadRole ? [
|
|
{ id: "manastorm-download-cancel", run: () => setPendingDownloadRole(null), enabled: !downloading },
|
|
{ id: "manastorm-download-confirm", run: () => void confirmDownload(), enabled: !downloading },
|
|
] : [
|
|
...MANASTORM_PARTY_SIZES.map((size) => ({
|
|
id: `manastorm-party-${size}`,
|
|
run: () => setPartySize(size),
|
|
enabled: !onlineGroup || (size >= groupHumanCount && (fillWithAi || size === groupHumanCount)),
|
|
})),
|
|
{ id: "manastorm-start-fresh", run: () => setResume(false) },
|
|
...(checkpoint > 1 ? [{ id: "manastorm-start-resume", run: () => setResume(true) }] : []),
|
|
...(spellLoadoutModel?.options.map((spell) => ({
|
|
id: `manastorm-spell-${spell.id}`,
|
|
run: () => toggleSpell(spell.id),
|
|
})) ?? []),
|
|
...availableRoles.map((role) => ({
|
|
id: `manastorm-role-${role}`,
|
|
run: () => { if (canEnter) void enterAsRole(role); },
|
|
enabled: canEnter,
|
|
})),
|
|
{ id: "manastorm-back", run: returnToMainMenu },
|
|
], [
|
|
availableRoles,
|
|
canEnter,
|
|
checkpoint,
|
|
contentState.installedRevision,
|
|
groupHumanCount,
|
|
fillWithAi,
|
|
onlineGroup,
|
|
effectivePartySize,
|
|
downloading,
|
|
pendingDownloadRole,
|
|
returnToMainMenu,
|
|
selectedSpellIds,
|
|
spellLoadoutModel,
|
|
startingLevel,
|
|
]);
|
|
const controller = useMenuController(actions, {
|
|
initialId: "manastorm-party-5",
|
|
onBack: pendingDownloadRole
|
|
? () => { if (!downloading) setPendingDownloadRole(null); }
|
|
: returnToMainMenu,
|
|
});
|
|
|
|
const race = character ? raceById(character.raceId) : null;
|
|
const characterClass = character ? classById(character.classId) : null;
|
|
if (readinessOpen) return <CompanionReadinessScreen onClose={() => setReadinessOpen(false)} />;
|
|
|
|
return (
|
|
<DualDisplayFrame
|
|
contextLabel="Manastorm details"
|
|
top={
|
|
<FrontSurface className="manastorm-select-screen" ariaLabel="Manastorm selection">
|
|
<header className="manastorm-select-header">
|
|
<BrandMark compact />
|
|
<div>
|
|
<span>Main menu / Manastorms</span>
|
|
<strong>Enter the storm</strong>
|
|
</div>
|
|
{character && race && characterClass ? (
|
|
<p><b>{character.name}</b><small>Level {character.level} {race.name} {characterClass.name}</small></p>
|
|
) : null}
|
|
</header>
|
|
|
|
<main className="manastorm-select-content">
|
|
<section className="manastorm-select-intro">
|
|
<p className="front-kicker">Ascension expedition</p>
|
|
<h1>Manastorms</h1>
|
|
<p><strong>{modeLabel} progression</strong></p>
|
|
<p>
|
|
Break Chaotic Link by clearing guardians or challenge the empowered boss early,
|
|
claim the reward, and cross the portal. Special encounters every five levels
|
|
unlock the next checkpoint.
|
|
</p>
|
|
{!canEnter ? (
|
|
<p className="front-notice" role="status">
|
|
{!modeEnabled
|
|
? `${modeLabel} Manastorms are currently disabled by a Game Master.`
|
|
: "No bosses are currently marked Ready for this Manastorm mode."}
|
|
</p>
|
|
) : null}
|
|
{canEnter && missingContentPackIds.length > 0 ? (
|
|
<p className="front-notice" role="status">
|
|
{missingContentPackIds.length} Manastorm map {missingContentPackIds.length === 1 ? "pack is" : "packs are"} not on this device. Choose a role to download them and continue.
|
|
</p>
|
|
) : null}
|
|
<dl>
|
|
<div><dt>Catalog maps</dt><dd>{MANASTORM_CATALOG_SUMMARY.maps}</dd></div>
|
|
<div><dt>Streamed maps</dt><dd>{GENERATED_MANASTORM_STAGE_ASSET_PACKAGE_COUNT}</dd></div>
|
|
<div><dt>Encounter records</dt><dd>{MANASTORM_CATALOG_SUMMARY.encounters}</dd></div>
|
|
<div><dt>Bedlam Bullions</dt><dd>{progress.currencies.bedlamBullions}</dd></div>
|
|
<div><dt>Bonzo Bolts</dt><dd>{progress.currencies.bonzoBolts}</dd></div>
|
|
<div><dt>Cache chance</dt><dd>{cacheChance}%</dd></div>
|
|
<div><dt>Bad-luck protection</dt><dd>{modeProgress.cacheMeter} / 5 misses</dd></div>
|
|
<div>
|
|
<dt>Next reward preview</dt>
|
|
<dd>
|
|
{rewardPreview
|
|
? `${rewardPreview.currencyAmount} ${rewardPreview.currencyId.replace(/([A-Z])/g, " $1").toLowerCase()}`
|
|
: "Unavailable"}
|
|
</dd>
|
|
</div>
|
|
<div><dt>Active spell picks</dt><dd>{selectedSpellIds.length || "None selected"}</dd></div>
|
|
</dl>
|
|
</section>
|
|
|
|
<section className="manastorm-role-select" aria-labelledby="manastorm-role-title">
|
|
<header>
|
|
<p className="front-kicker">Run setup</p>
|
|
<h2 id="manastorm-role-title">Build your expedition</h2>
|
|
<small>Progress and first clears are tracked separately for every party size.</small>
|
|
</header>
|
|
<OnlineGroupPanel />
|
|
<CompanionPoolControls onOpenReadiness={() => setReadinessOpen(true)} compact />
|
|
|
|
<div className="manastorm-setup-block">
|
|
<strong>Party size</strong>
|
|
<div className="manastorm-party-size-grid">
|
|
{MANASTORM_PARTY_SIZES.map((size) => (
|
|
<ControllerButton
|
|
key={size}
|
|
controlId={`manastorm-party-${size}`}
|
|
selectedId={controller.selectedId}
|
|
select={controller.select}
|
|
className={`manastorm-size-option ${effectivePartySize === size ? "is-active" : ""}`}
|
|
type="button"
|
|
disabled={Boolean(onlineGroup && (size < groupHumanCount || (!fillWithAi && size !== groupHumanCount)))}
|
|
onClick={() => setPartySize(size)}
|
|
>
|
|
{size}
|
|
</ControllerButton>
|
|
))}
|
|
</div>
|
|
<small>
|
|
{onlineGroup
|
|
? fillWithAi
|
|
? `${groupHumanCount} player${groupHumanCount === 1 ? "" : "s"} + ${Math.max(0, effectivePartySize - groupHumanCount)} AI teammate${effectivePartySize - groupHumanCount === 1 ? "" : "s"}`
|
|
: `${groupHumanCount} grouped player${groupHumanCount === 1 ? "" : "s"} · AI fill off`
|
|
: partySize === 1 ? "Solo" : `You + ${partySize - 1} companion${partySize === 2 ? "" : "s"}`}
|
|
{" · "}Highest clear {partyProgress.highestLevel}
|
|
</small>
|
|
</div>
|
|
|
|
<div className="manastorm-setup-block">
|
|
<strong>Entry level</strong>
|
|
<div className="manastorm-entry-grid">
|
|
<ControllerButton
|
|
controlId="manastorm-start-fresh"
|
|
selectedId={controller.selectedId}
|
|
select={controller.select}
|
|
className={`manastorm-entry-option ${!resume || checkpoint === 1 ? "is-active" : ""}`}
|
|
type="button"
|
|
disabled={!canEnter}
|
|
onClick={() => setResume(false)}
|
|
>
|
|
Start at 1
|
|
</ControllerButton>
|
|
{checkpoint > 1 ? (
|
|
<ControllerButton
|
|
controlId="manastorm-start-resume"
|
|
selectedId={controller.selectedId}
|
|
select={controller.select}
|
|
className={`manastorm-entry-option ${resume ? "is-active" : ""}`}
|
|
type="button"
|
|
onClick={() => setResume(true)}
|
|
>
|
|
Resume at {checkpoint}
|
|
</ControllerButton>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="manastorm-setup-block">
|
|
<strong>
|
|
Local active-spell loadout
|
|
{" · "}
|
|
{selectedSpellIds.length} / {spellLoadoutModel?.maxSelections ?? 0}
|
|
</strong>
|
|
<small>
|
|
Client-grounded spells only. The installed Manastorm tables contain no
|
|
authoritative spell-choice list, so these picks locally replace the first four
|
|
base controls for this run.
|
|
</small>
|
|
<div className="manastorm-spell-grid">
|
|
{spellLoadoutModel?.options.map((spell) => {
|
|
const selected = selectedSpellIds.includes(spell.id);
|
|
const capped = !selected
|
|
&& selectedSpellIds.length >= spellLoadoutModel.maxSelections;
|
|
return (
|
|
<AbilityTooltipTarget key={spell.id} ability={spell}>
|
|
<ControllerButton
|
|
key={spell.id}
|
|
controlId={`manastorm-spell-${spell.id}`}
|
|
selectedId={controller.selectedId}
|
|
select={controller.select}
|
|
className={`manastorm-spell-option ${selected ? "is-active" : ""}`}
|
|
type="button"
|
|
aria-disabled={capped}
|
|
onClick={() => { if (!capped) toggleSpell(spell.id); }}
|
|
title={`${spell.description} · Client Spell.dbc ID ${spell.dbcSpellId}`}
|
|
>
|
|
<img src={spell.icon} alt="" aria-hidden="true" />
|
|
<span>
|
|
<b>{spell.name}</b>
|
|
<small>Spell {spell.dbcSpellId}</small>
|
|
</span>
|
|
</ControllerButton>
|
|
</AbilityTooltipTarget>
|
|
);
|
|
})}
|
|
{!spellLoadoutModel?.options.length ? (
|
|
<p className="front-notice">
|
|
No imported active spells are available at this character level.
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
|
|
<strong className="manastorm-role-label">Party role</strong>
|
|
<div className="manastorm-role-grid">
|
|
{availableRoles.map((role) => (
|
|
<ControllerButton
|
|
key={role}
|
|
controlId={`manastorm-role-${role}`}
|
|
selectedId={controller.selectedId}
|
|
select={controller.select}
|
|
className={`dungeon-role-option dungeon-role-option--${role}`}
|
|
type="button"
|
|
disabled={!canEnter}
|
|
onFocus={() => preloadRole(role)}
|
|
onPointerEnter={() => preloadRole(role)}
|
|
onClick={() => { if (canEnter) void enterAsRole(role); }}
|
|
>
|
|
<b aria-hidden="true">{ROLE_DETAILS[role].sigil}</b>
|
|
<span>
|
|
<strong>{onlineGroup && session?.ownerId !== onlineGroup.leaderAccountId ? "Ready" : "Enter"} as {PARTY_ROLE_LABELS[role]}</strong>
|
|
<small>{ROLE_DETAILS[role].summary}</small>
|
|
</span>
|
|
</ControllerButton>
|
|
))}
|
|
</div>
|
|
{notice ? <p className="front-notice" role="status">{notice}</p> : null}
|
|
<ControllerButton
|
|
controlId="manastorm-back"
|
|
selectedId={controller.selectedId}
|
|
select={controller.select}
|
|
className="front-button manastorm-select-back"
|
|
type="button"
|
|
onClick={returnToMainMenu}
|
|
>
|
|
Back to main menu
|
|
</ControllerButton>
|
|
</section>
|
|
</main>
|
|
<ControllerLegend back />
|
|
{pendingDownloadRole && (
|
|
<div className="front-modal-backdrop">
|
|
<section className="front-confirm" role="alertdialog" aria-modal="true" aria-labelledby="manastorm-download-title">
|
|
<p className="front-kicker">Download required</p>
|
|
<h2 id="manastorm-download-title">Download {modeLabel} Manastorm maps?</h2>
|
|
<p>
|
|
This mode can draw {missingContentPackIds.length} map {missingContentPackIds.length === 1 ? "pack" : "packs"} that {missingContentPackIds.length === 1 ? "is" : "are"} not on this device. Download and verify the required files now, then enter the storm?
|
|
</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="manastorm-download-cancel"
|
|
selectedId={controller.selectedId}
|
|
select={controller.select}
|
|
className="front-button"
|
|
type="button"
|
|
disabled={downloading}
|
|
onClick={() => setPendingDownloadRole(null)}
|
|
>
|
|
Not now
|
|
</ControllerButton>
|
|
<ControllerButton
|
|
controlId="manastorm-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 />}
|
|
/>
|
|
);
|
|
}
|