Files
healer-man/src/ui/RandomizedCreatureReadinessScreen.tsx
T
2026-09-20 19:27:37 -04:00

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>
);
}