Release Healer Man 0.1.58

This commit is contained in:
phenom committed 2026-09-20 16:46:17 -04:00
1 parent 14e105642c
commit 246b4832b0
38 files changed
+952 -93

No files matched your search

+132
View File
@@ -0,0 +1,132 @@
import { 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,
STORMWIND_STOCKADE_DUNGEON_ID,
dungeonContentPackId,
} from "../game/dungeonContentSource";
import { randomizedBiteSizedCreatureCatalog } from "../game/dungeons/randomizedBiteSizedStockades";
import { dungeonById } from "../game/dungeonCatalog";
import { 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";
installDungeonShellRuntime();
const DUNGEON = dungeonById(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID);
const CANDIDATES = randomizedBiteSizedCreatureCatalog(requireDungeonDefinition(STORMWIND_STOCKADE_DUNGEON_ID));
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 [downloading, setDownloading] = useState(false);
const [error, setError] = useState("");
const [readinessOpen, setReadinessOpen] = useState(false);
const character = activeCharacter ?? rosterCharacter;
const specialization = specializationById(character?.specializationId);
const roles = character
? character.categoryId === "rom" ? partyRolesForClass(character.classId) : specialization ? [specialization.role] : []
: [];
const installed = dungeonContentIsUsable(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID);
const mobCount = CANDIDATES.filter((candidate) => candidate.kind === "mob").length;
const bossCount = CANDIDATES.filter((candidate) => candidate.kind === "boss").length;
const launch = async (role: PartyRole) => {
if (!character || !DUNGEON?.available) return;
if (!onlineGroup || !session || session.kind !== "account") {
enterDungeon(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID, role, {
difficultyId: requireDungeonDefinition(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID).defaultDifficultyId,
bossDifficulty,
freshRun: true,
});
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: RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID,
difficultyId: requireDungeonDefinition(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID).defaultDifficultyId,
bossDifficulty,
partySize: 5,
fillWithAi: true,
includeBetaSpecs: false,
});
};
const install = async () => {
setDownloading(true); setError("");
try {
await installContentPack(dungeonContentPackId(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID));
if (!dungeonContentIsUsable(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID)) {
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 <CompanionReadinessScreen onClose={() => setReadinessOpen(false)} />;
return (
<FrontSurface className="randomized-bite-sized-screen" ariaLabel="Randomized Bite-Sized dungeon">
<header className="randomized-bite-sized-screen__header">
<BrandMark compact />
<div><span>Main menu / Randomized Bite-Sized</span><strong>Cellblock Shuffle</strong></div>
<button className="front-button" type="button" onClick={onClose}>Main menu</button>
</header>
<main className="randomized-bite-sized-screen__layout">
<section className="randomized-bite-sized-screen__hero">
<p className="front-kicker">A different cellblock every run</p>
<h1>Randomized Bite-Sized</h1>
<p>Fight through the short Stockades route with every ordinary enemy slot rerolled, then face one randomly selected warden.</p>
<div className="randomized-bite-sized-screen__odds">
<div><strong>{mobCount}</strong><span>mob types in the shuffle</span></div>
<div><strong>{bossCount}</strong><span>possible final bosses</span></div>
<div><strong>1</strong><span>boss per run</span></div>
</div>
<p className="front-notice">The random lineup is locked to the run seed, so online party members and resumed sessions see the same enemies.</p>
<OnlineGroupPanel forceAiFill />
<CompanionPoolControls onOpenReadiness={() => setReadinessOpen(true)} readyOnly />
</section>
<section className="randomized-bite-sized-screen__launch">
<p className="front-kicker">Run setup</p>
<h2>{DUNGEON?.title ?? "Cellblock Shuffle"}</h2>
<DungeonDifficultySlider value={bossDifficulty} onChange={setBossDifficulty} selectedId="" select={() => undefined} />
{!installed ? (
<button className="front-button front-button--primary" type="button" disabled={downloading} onClick={() => void install()}>{downloading ? "Installing…" : "Install Stockades content"}</button>
) : roles.length ? (
<div className="randomized-bite-sized-screen__roles">
<strong>Enter as</strong>
{roles.map((role) => <button className="front-button front-button--primary" key={role} type="button" disabled={!DUNGEON?.available} onClick={() => void launch(role)}>{PARTY_ROLE_LABELS[role]}</button>)}
</div>
) : <p className="front-notice">Choose a specialization in your Class Hall before entering.</p>}
{!DUNGEON?.available && <p className="front-notice">This randomized route is not enterable in the current content build.</p>}
{(error || notice) && <p className="front-notice" role="status">{error || notice}</p>}
</section>
</main>
</FrontSurface>
);
}