99 lines
6.1 KiB
TypeScript
99 lines
6.1 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
|
import {
|
|
CREATURE_READINESS_LABELS,
|
|
CREATURE_READINESS_STATUSES,
|
|
type CreatureReadinessStatus,
|
|
useRandomizedCreatureReadiness,
|
|
} from "../app/randomizedCreatureReadiness";
|
|
import { useShellStore } from "../app/shellStore";
|
|
import type { RandomizedCreatureKind } from "../game/dungeons/randomizedBiteSizedStockades";
|
|
import { RANDOMIZED_BITE_SIZED_CREATURE_CATALOG } from "../game/dungeonRegistry";
|
|
import { BrandMark, FrontSurface } from "./FrontSurface";
|
|
|
|
const CREATURES = RANDOMIZED_BITE_SIZED_CREATURE_CATALOG;
|
|
|
|
type KindFilter = "all" | RandomizedCreatureKind;
|
|
type StatusFilter = "all" | CreatureReadinessStatus;
|
|
|
|
export function RandomizedCreatureReadinessScreen({ onClose }: { readonly onClose: () => void }) {
|
|
const session = useShellStore((state) => state.session);
|
|
const reviews = useRandomizedCreatureReadiness((state) => state.reviews);
|
|
const loading = useRandomizedCreatureReadiness((state) => state.loading);
|
|
const error = useRandomizedCreatureReadiness((state) => state.error);
|
|
const refresh = useRandomizedCreatureReadiness((state) => state.refresh);
|
|
const save = useRandomizedCreatureReadiness((state) => state.save);
|
|
const [query, setQuery] = useState("");
|
|
const [kind, setKind] = useState<KindFilter>("all");
|
|
const [status, setStatus] = useState<StatusFilter>("all");
|
|
const [selectedKey, setSelectedKey] = useState(CREATURES[0]?.key ?? "");
|
|
|
|
useEffect(() => { void refresh(); }, [refresh]);
|
|
const statusOf = (key: string): CreatureReadinessStatus => reviews[key]?.status ?? "needs-testing";
|
|
const filtered = useMemo(() => CREATURES.filter((creature) => (
|
|
(kind === "all" || creature.kind === kind)
|
|
&& (status === "all" || statusOf(creature.key) === status)
|
|
&& (!query.trim() || creature.name.toLowerCase().includes(query.trim().toLowerCase())
|
|
|| String(creature.sourceEntry).includes(query.trim()))
|
|
)), [kind, query, reviews, status]);
|
|
const selected = CREATURES.find((creature) => creature.key === selectedKey) ?? filtered[0] ?? null;
|
|
const counts = Object.fromEntries(CREATURE_READINESS_STATUSES.map((candidate) => [
|
|
candidate,
|
|
CREATURES.filter((creature) => statusOf(creature.key) === candidate).length,
|
|
])) as Record<CreatureReadinessStatus, number>;
|
|
|
|
return (
|
|
<FrontSurface className="randomized-creature-readiness" ariaLabel="Randomized creature readiness">
|
|
<header className="randomized-creature-readiness__header">
|
|
<BrandMark compact />
|
|
<div><p className="eyebrow">Game Master tools</p><h1>Randomized creature readiness</h1></div>
|
|
<div>
|
|
<button className="front-button" type="button" disabled={loading} onClick={() => void refresh()}>Refresh</button>
|
|
<button className="front-button" type="button" onClick={onClose}>Back</button>
|
|
</div>
|
|
</header>
|
|
<section className="randomized-creature-readiness__summary" aria-label="Readiness counts">
|
|
{CREATURE_READINESS_STATUSES.map((candidate) => (
|
|
<div key={candidate}><strong>{counts[candidate]}</strong><span>{CREATURE_READINESS_LABELS[candidate]}</span></div>
|
|
))}
|
|
</section>
|
|
<main className="randomized-creature-readiness__layout">
|
|
<section className="randomized-creature-readiness__catalog">
|
|
<div className="randomized-creature-readiness__filters">
|
|
<label><span>Search</span><input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Name or source entry" /></label>
|
|
<label><span>Kind</span><select value={kind} onChange={(event) => setKind(event.target.value as KindFilter)}>
|
|
<option value="all">All</option><option value="mob">Mobs</option><option value="boss">Bosses</option>
|
|
</select></label>
|
|
<label><span>Status</span><select value={status} onChange={(event) => setStatus(event.target.value as StatusFilter)}>
|
|
<option value="all">All</option>{CREATURE_READINESS_STATUSES.map((candidate) => <option key={candidate} value={candidate}>{CREATURE_READINESS_LABELS[candidate]}</option>)}
|
|
</select></label>
|
|
</div>
|
|
<div className="randomized-creature-readiness__list">
|
|
{filtered.map((creature) => (
|
|
<button key={creature.key} className={creature.key === selected?.key ? "is-active" : ""} type="button" onClick={() => setSelectedKey(creature.key)}>
|
|
<span><strong>{creature.name}</strong><small>{creature.kind} · entry {creature.sourceEntry}</small></span>
|
|
<em className={`creature-readiness creature-readiness--${statusOf(creature.key)}`}>{CREATURE_READINESS_LABELS[statusOf(creature.key)]}</em>
|
|
</button>
|
|
))}
|
|
{!filtered.length && <p className="front-notice">No creatures match these filters.</p>}
|
|
</div>
|
|
</section>
|
|
<section className="randomized-creature-readiness__details">
|
|
{selected ? <>
|
|
<p className="front-kicker">{selected.kind === "boss" ? "Boss candidate" : "Mob candidate"}</p>
|
|
<h2>{selected.name}</h2>
|
|
<dl><div><dt>Source</dt><dd>{selected.sourceDungeonTitle}</dd></div><div><dt>Entity</dt><dd>{selected.entityId}</dd></div><div><dt>Source entry</dt><dd>{selected.sourceEntry}</dd></div></dl>
|
|
<p>Current status: <strong>{CREATURE_READINESS_LABELS[statusOf(selected.key)]}</strong></p>
|
|
<div className="randomized-creature-readiness__status-actions">
|
|
{CREATURE_READINESS_STATUSES.map((candidate) => (
|
|
<button className={`front-button ${candidate === statusOf(selected.key) ? "front-button--primary" : ""}`} key={candidate} type="button" disabled={loading || !session} onClick={() => session && void save(session, selected, candidate)}>{CREATURE_READINESS_LABELS[candidate]}</button>
|
|
))}
|
|
</div>
|
|
{reviews[selected.key] && <small>Last reviewed by {reviews[selected.key].reviewedBy} on {new Date(reviews[selected.key].reviewedAt).toLocaleString()}.</small>}
|
|
</> : <p className="front-notice">Select a creature to review it.</p>}
|
|
{error && <p className="front-notice" role="alert">{error}</p>}
|
|
</section>
|
|
</main>
|
|
</FrontSurface>
|
|
);
|
|
}
|