Release Healer Man 0.1.12

This commit is contained in:
phenom committed 2026-09-01 14:35:56 -04:00
1 parent 13970d03c8
commit 65e24f644e
201 files changed
+5061 -123

No files matched your search

+59 -2
View File
@@ -1,9 +1,10 @@
import { useMemo, useState } from "react";
import { enterGmManastormTest } from "../app/gmManastormRuntime";
import { enterGmManastormTest, enterRandomGmReview } from "../app/gmManastormRuntime";
import { installDungeonShellRuntime } from "../app/dungeonShellRuntime";
import { useShellStore } from "../app/shellStore";
import {
bossAdminRecordForStatus,
GM_REVIEW_NOTE_MAX_LENGTH,
isGameMasterSession,
resolvedBossAdminCatalog,
type ManastormBossStatus,
@@ -11,6 +12,11 @@ import {
} from "../game/manastormAdminConfig";
import { useManastormAdminStore } from "../game/manastormAdminStore";
import { useGameStore } from "../game/store";
import {
configWithGmReviewNote,
gmReviewQueue,
type GmReviewKind,
} from "../game/gmContentReview";
import { BrandMark, FrontSurface } from "./FrontSurface";
const STATUS_LABELS: Readonly<Record<ManastormBossStatus, string>> = {
@@ -51,6 +57,9 @@ export function GmAdminScreen() {
const [poolFilter, setPoolFilter] = useState<"all" | "public" | "excluded">("all");
const [selectedKey, setSelectedKey] = useState<string | null>(null);
const catalog = useMemo(() => resolvedBossAdminCatalog(draft), [draft]);
const randomQueueCounts = useMemo(() => Object.fromEntries(
(["dungeon", "boss", "manastorm"] as const).map((kind) => [kind, gmReviewQueue(kind, draft).length]),
) as Readonly<Record<GmReviewKind, number>>, [draft]);
const dungeonOptions = useMemo(() => [...new Map(catalog.map((entry) => [entry.dungeonTitle, entry.dungeonTitle])).keys()].sort(), [catalog]);
const filtered = useMemo(() => {
const query = search.trim().toLowerCase();
@@ -60,7 +69,7 @@ export function GmAdminScreen() {
&& (sourceFilter === "all" || entry.source.includes(sourceFilter))
&& (runnableFilter === "all" || (runnableFilter === "runnable" ? entry.runnable : !entry.runnable))
&& (poolFilter === "all" || (poolFilter === "public" ? entry.inPublicPool : !entry.inPublicPool))
&& (!query || `${entry.name} ${entry.dungeonTitle} ${entry.source.join(" ")}`.toLowerCase().includes(query))
&& (!query || `${entry.name} ${entry.dungeonTitle} ${entry.source.join(" ")} ${entry.reviewNote}`.toLowerCase().includes(query))
));
}, [catalog, dungeonFilter, poolFilter, runnableFilter, search, sourceFilter, statusFilter]);
const selected = catalog.find((entry) => entry.key === selectedKey) ?? filtered[0] ?? null;
@@ -96,6 +105,11 @@ export function GmAdminScreen() {
});
};
const setReviewNote = (key: string, note: string) => {
const next = configWithGmReviewNote(draft, { kind: "boss", bossKey: key }, note);
if (next) replaceDraft(next);
};
const editInDungeon = () => {
if (!selected?.dungeonId) return;
setEditingBossKey(selected.key);
@@ -126,6 +140,28 @@ export function GmAdminScreen() {
<main className="gm-admin-layout">
<aside className="gm-admin-options">
<section className="gm-random-qa" aria-labelledby="gm-random-qa-title">
<p className="front-kicker">Random QA</p>
<h2 id="gm-random-qa-title">Review queue</h2>
<p>Needs tested first, then Not ready. Ready content stays out of the draw.</p>
{(["dungeon", "boss", "manastorm"] as const).map((kind) => {
const label = kind === "dungeon" ? "Dungeon" : kind === "boss" ? "Boss" : "Manastorm";
const count = randomQueueCounts[kind];
return (
<button
key={kind}
type="button"
className="front-button"
disabled={dirty || saving || count === 0}
title={dirty ? "Save changes before testing." : count === 0 ? `All runnable ${label.toLowerCase()} content is Ready.` : undefined}
onClick={() => enterRandomGmReview(kind)}
>
<span>Random {label}</span>
<small>{count ? `${count} queued` : "All reviewed"}</small>
</button>
);
})}
</section>
<p className="front-kicker">Global switches</p>
<h2>Storm rules</h2>
{GLOBAL_OPTIONS.map((option) => (
@@ -220,6 +256,27 @@ export function GmAdminScreen() {
>{STATUS_LABELS[status]}</button>
))}
</div>
<section className="gm-review-note-editor" aria-label={`Review note for ${selected.name}`}>
<header>
<strong>Review note</strong>
<small>{selected.reviewNote.length} / {GM_REVIEW_NOTE_MAX_LENGTH}</small>
</header>
<textarea
aria-label={`Review note for ${selected.name}`}
value={selected.reviewNote}
maxLength={GM_REVIEW_NOTE_MAX_LENGTH}
rows={5}
disabled={saving}
placeholder="Record what needs testing or fixing."
onChange={(event) => setReviewNote(selected.key, event.currentTarget.value)}
/>
<footer>
<small>{selected.reviewedAt && selected.reviewedBy
? `Last reviewed by ${selected.reviewedBy} · ${new Date(selected.reviewedAt).toLocaleString()}`
: "Not yet reviewed."}</small>
<button type="button" disabled={saving || !selected.reviewNote} onClick={() => setReviewNote(selected.key, "")}>Clear note</button>
</footer>
</section>
<div className="gm-boss-actions">
<button type="button" className="front-button" disabled={!selected.dungeonId} onClick={editInDungeon}>Edit in dungeon</button>
<button type="button" className="front-button" disabled={!selected.dungeonId} onClick={enterSelectedDungeon}>Enter dungeon</button>