import { useEffect, useState } from "react"; import { selectedCharacter, useShellStore } from "../app/shellStore"; import { installDungeonShellRuntime } from "../app/dungeonShellRuntime"; import { useOnlineGroupStore } from "../app/onlineGroupStore"; import { specializationById } from "../game/specializationCatalog"; import { partyRolesForClass, PARTY_ROLE_LABELS, type PartyRole } from "../game/partyStore"; import { dungeonContentIsUsable, installContentPack } from "../content/contentManager"; import { useContentManagerSnapshot } from "../content/useContentManager"; import { RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID, RANDOMIZED_BITE_SIZED_AVAILABLE_DUNGEON_ID, dungeonContentPackId, } from "../game/dungeonContentSource"; import { dungeonById } from "../game/dungeonCatalog"; import { dungeonEnabledForQueue } from "../game/dungeonQueuePolicy"; import { loadDungeonQueuePolicy, useDungeonQueuePolicyStore } from "../app/dungeonQueuePolicyStore"; 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"; import { CompanionPoolControls } from "./CompanionPoolControls"; import { CompanionReadinessScreen } from "./CompanionReadinessScreen"; import { ensureRandomizedBiteSizedRunContent } from "../app/randomizedBiteSizedContent"; import { BITE_SIZED_LAYOUT_CATALOG } from "../../shared/bite-sized-layouts.mjs"; installDungeonShellRuntime(); export function RandomizedBiteSizedScreen({ onClose }: { readonly onClose: () => void }) { useContentManagerSnapshot(); const activeCharacter = useShellStore((state) => state.activeCharacter); const rosterCharacter = useShellStore(selectedCharacter); const enterDungeon = useShellStore((state) => state.enterDungeon); const session = useShellStore((state) => state.session); const notice = useShellStore((state) => state.notice); const onlineGroup = useOnlineGroupStore((state) => state.snapshot.group); 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); const queuePolicy = useDungeonQueuePolicyStore((state) => state.config); const refreshQueuePolicy = useDungeonQueuePolicyStore((state) => state.refresh); useEffect(() => { void refreshQueuePolicy(session); }, [refreshQueuePolicy, session]); const character = activeCharacter ?? rosterCharacter; const specialization = specializationById(character?.specializationId); const roles = character ? character.categoryId === "rom" ? partyRolesForClass(character.classId) : specialization ? [specialization.role] : [] : []; 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 = BITE_SIZED_LAYOUT_CATALOG.dungeons.some((d) => dungeonContentIsUsable(d.sourceDungeonId)); 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)); const launch = async (role: PartyRole) => { if (!character || !DUNGEON?.available) return; const policy = await loadDungeonQueuePolicy(); const selectedDungeon = DUNGEON && dungeonEnabledForQueue(policy, DUNGEON) ? DUNGEON : null; if (!selectedDungeon) { setError("No Randomized Bite-Sized location is enabled for this queue."); return; } if (!onlineGroup || !session || session.kind !== "account") { const runId = globalThis.crypto.randomUUID(); setDownloading(true); setError(""); try { await ensureRandomizedBiteSizedRunContent(runId, undefined, selectedDungeon.id); enterDungeon(selectedDungeon.id, role, { difficultyId: requireDungeonDefinition(selectedDungeon.id).defaultDifficultyId, bossDifficulty, freshRun: true, runId, }); } catch (caught) { setError(caught instanceof Error ? caught.message : "The randomized creature download failed."); } finally { setDownloading(false); } 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: "dungeon", contentId: selectedDungeon.id, difficultyId: requireDungeonDefinition(selectedDungeon.id).defaultDifficultyId, bossDifficulty, partySize: 5, fillWithAi: true, includeBetaSpecs: false, }); }; const install = async () => { setDownloading(true); setError(""); try { await installContentPack(dungeonContentPackId(dungeonId)); if (!dungeonContentIsUsable(dungeonId)) { throw new Error("The Stockades content pack could not be verified."); } } catch (caught) { setError(caught instanceof Error ? caught.message : "The content download failed."); } finally { setDownloading(false); } }; if (readinessOpen) return setReadinessOpen(false)} />; return (
Main menu / Randomized Bite-SizedCellblock Shuffle

A different lineup every run

Randomized Bite-Sized

Visit a random saved spot in an eligible dungeon. Each mob point and the final boss draw creatures from the pool you choose.

{mobCount}mob types in the shuffle
{bossCount}possible final bosses
1boss per run

The random lineup is locked to the run seed, so online party members and resumed sessions see the same enemies.

setReadinessOpen(true)} readyOnly />

Run setup

Creature pool

{queueEnabled ? DUNGEON?.title ?? "Cellblock Shuffle" : "No enabled location"}

undefined} /> {!queueEnabled ? null : !installed ? ( ) : roles.length ? (
Enter as {roles.map((role) => )}
) :

Choose a specialization in your Class Hall before entering.

} {!DUNGEON?.available &&

This randomized route is not enterable in the current content build.

} {!queueEnabled &&

No Randomized Bite-Sized location is enabled in GM queue settings.

} {(error || notice) &&

{error || notice}

}
); }