Implements PF2e as an alternative game system alongside D&D 5e/5.5e. Settings modal "Game System" selector switches conditions, bestiary, stat block layout, and initiative calculation between systems. - Valued conditions with increment/decrement UX (Clumsy 2, Frightened 3) - 2,502 PF2e creatures from bundled search index (77 sources) - PF2e stat block: level, traits, Perception, Fort/Ref/Will, ability mods - Perception-based initiative rolling - System-scoped source cache (D&D and PF2e sources don't collide) - Backwards-compatible condition rehydration (ConditionId[] → ConditionEntry[]) - Difficulty indicator hidden in PF2e mode (excluded from MVP) Closes dostulata/initiative#19 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
119 lines
3.6 KiB
TypeScript
119 lines
3.6 KiB
TypeScript
import { Loader2 } from "lucide-react";
|
|
import { useId, useState } from "react";
|
|
import { useAdapters } from "../contexts/adapter-context.js";
|
|
import { useBestiaryContext } from "../contexts/bestiary-context.js";
|
|
import { useBulkImportContext } from "../contexts/bulk-import-context.js";
|
|
import { useRulesEditionContext } from "../contexts/rules-edition-context.js";
|
|
import { useSidePanelContext } from "../contexts/side-panel-context.js";
|
|
import { Button } from "./ui/button.js";
|
|
import { Input } from "./ui/input.js";
|
|
|
|
const DND_BASE_URL =
|
|
"https://raw.githubusercontent.com/5etools-mirror-3/5etools-src/main/data/bestiary/";
|
|
|
|
const PF2E_BASE_URL =
|
|
"https://raw.githubusercontent.com/Pf2eToolsOrg/Pf2eTools/dev/data/bestiary/";
|
|
|
|
export function BulkImportPrompt() {
|
|
const { bestiaryIndex, pf2eBestiaryIndex } = useAdapters();
|
|
const { fetchAndCacheSource, isSourceCached, refreshCache } =
|
|
useBestiaryContext();
|
|
const { state: importState, startImport, reset } = useBulkImportContext();
|
|
const { dismissPanel } = useSidePanelContext();
|
|
const { edition } = useRulesEditionContext();
|
|
|
|
const indexPort = edition === "pf2e" ? pf2eBestiaryIndex : bestiaryIndex;
|
|
const defaultUrl = edition === "pf2e" ? PF2E_BASE_URL : DND_BASE_URL;
|
|
const [baseUrl, setBaseUrl] = useState(defaultUrl);
|
|
const baseUrlId = useId();
|
|
const totalSources = indexPort.getAllSourceCodes().length;
|
|
|
|
const handleStart = (url: string) => {
|
|
startImport(url, fetchAndCacheSource, isSourceCached, refreshCache);
|
|
};
|
|
|
|
const handleDone = () => {
|
|
dismissPanel();
|
|
reset();
|
|
};
|
|
|
|
if (importState.status === "complete") {
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<div className="rounded-md border border-green-500/50 bg-green-500/10 px-3 py-2 text-green-400 text-sm">
|
|
All sources loaded
|
|
</div>
|
|
<Button onClick={handleDone}>Done</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (importState.status === "partial-failure") {
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<div className="rounded-md border border-yellow-500/50 bg-yellow-500/10 px-3 py-2 text-sm text-yellow-400">
|
|
Loaded {importState.completed}/{importState.total} sources (
|
|
{importState.failed} failed)
|
|
</div>
|
|
<Button onClick={handleDone}>Done</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (importState.status === "loading") {
|
|
const processed = importState.completed + importState.failed;
|
|
const pct =
|
|
importState.total > 0
|
|
? Math.round((processed / importState.total) * 100)
|
|
: 0;
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<div className="flex items-center gap-2 text-muted-foreground text-sm">
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
Loading sources... {processed}/{importState.total}
|
|
</div>
|
|
<div className="h-2 overflow-hidden rounded-full bg-muted">
|
|
<div
|
|
className="h-full rounded-full bg-primary transition-all"
|
|
style={{ width: `${pct}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// idle state
|
|
const isDisabled = !baseUrl.trim() || importState.status !== "idle";
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<div>
|
|
<h3 className="font-semibold text-foreground text-sm">
|
|
Import All Sources
|
|
</h3>
|
|
<p className="mt-1 text-muted-foreground text-xs">
|
|
Load stat block data for all {totalSources} sources at once.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<label htmlFor={baseUrlId} className="text-muted-foreground text-xs">
|
|
Base URL
|
|
</label>
|
|
<Input
|
|
id={baseUrlId}
|
|
type="url"
|
|
value={baseUrl}
|
|
onChange={(e) => setBaseUrl(e.target.value)}
|
|
className="text-xs"
|
|
/>
|
|
</div>
|
|
|
|
<Button onClick={() => handleStart(baseUrl)} disabled={isDisabled}>
|
|
Load All
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|