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

+130
View File
@@ -0,0 +1,130 @@
import { useMemo } from "react";
import { enterRandomGmReview } from "../app/gmManastormRuntime";
import { useShellStore } from "../app/shellStore";
import {
configWithGmReviewNote,
configWithGmReviewStatus,
describeGmReviewTarget,
gmReviewQueue,
} from "../game/gmContentReview";
import {
GM_REVIEW_NOTE_MAX_LENGTH,
isGameMasterSession,
type GmReadinessStatus,
} from "../game/manastormAdminConfig";
import { useManastormAdminStore } from "../game/manastormAdminStore";
import { useGameStore } from "../game/store";
const STATUS_LABELS: Readonly<Record<GmReadinessStatus, string>> = {
ready: "Ready",
"needs-tested": "Needs tested",
"not-ready": "Not ready",
};
const KIND_LABELS = {
dungeon: "Dungeon review",
boss: "Boss review",
manastorm: "Manastorm review",
} as const;
export function GmReviewCard() {
const session = useShellStore((state) => state.session);
const target = useGameStore((state) => state.activeGmReviewTarget);
const config = useManastormAdminStore((state) => state.config);
const draft = useManastormAdminStore((state) => state.draft);
const dirty = useManastormAdminStore((state) => state.dirty);
const saving = useManastormAdminStore((state) => state.saving);
const error = useManastormAdminStore((state) => state.error);
const replaceDraft = useManastormAdminStore((state) => state.replaceDraft);
const publish = useManastormAdminStore((state) => state.publish);
const descriptor = useMemo(
() => target ? describeGmReviewTarget(target, draft) : null,
[draft, target],
);
const nextCount = useMemo(
() => target ? gmReviewQueue(target.kind, config, target).length : 0,
[config, target],
);
if (!target || !descriptor || !isGameMasterSession(session)) return null;
const setStatus = (status: GmReadinessStatus) => {
if (descriptor.status === status) return;
const next = configWithGmReviewStatus(draft, target, status);
if (next) replaceDraft(next);
};
const setNote = (note: string) => {
const next = configWithGmReviewNote(draft, target, note);
if (next) replaceDraft(next);
};
const targetKey = target.kind === "dungeon" ? target.dungeonId : target.bossKey;
const notePreview = descriptor.note.replace(/\s+/g, " ").trim();
const reviewMetadata = descriptor.reviewedAt && descriptor.reviewedBy
? `${descriptor.reviewedBy} · ${new Date(descriptor.reviewedAt).toLocaleString()}`
: null;
return (
<section
className={`gm-review-card is-${descriptor.status}`}
aria-label={`${KIND_LABELS[target.kind]} for ${descriptor.name}`}
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => event.stopPropagation()}
onKeyDown={(event) => event.stopPropagation()}
onKeyUp={(event) => event.stopPropagation()}
>
<header>
<span>{KIND_LABELS[target.kind]}</span>
<b>{dirty ? "Unsaved" : STATUS_LABELS[descriptor.status]}</b>
</header>
<strong>{descriptor.name}</strong>
<small>{descriptor.dungeonTitle} · Map {descriptor.mapId}</small>
<div className="gm-review-card__statuses" aria-label="Readiness">
{(["ready", "needs-tested", "not-ready"] as const).map((status) => (
<button
key={status}
type="button"
className={descriptor.status === status ? "is-active" : ""}
disabled={saving || (status === "ready" && !descriptor.runnable)}
onClick={() => setStatus(status)}
>{STATUS_LABELS[status]}</button>
))}
</div>
<details key={`${target.kind}:${targetKey}`} className="gm-review-card__note">
<summary>
<span>Review note</span>
<small title={notePreview || undefined}>{notePreview || "Add context"}</small>
</summary>
<textarea
aria-label={`Review note for ${descriptor.name}`}
value={descriptor.note}
maxLength={GM_REVIEW_NOTE_MAX_LENGTH}
rows={3}
disabled={saving}
placeholder="What needs testing or fixing?"
onChange={(event) => setNote(event.currentTarget.value)}
/>
<div>
<span>{descriptor.note.length} / {GM_REVIEW_NOTE_MAX_LENGTH}</span>
<button type="button" disabled={saving || !descriptor.note} onClick={() => setNote("")}>Clear note</button>
</div>
{reviewMetadata ? <p>Last reviewed by {reviewMetadata}</p> : <p>Not yet reviewed.</p>}
</details>
{error ? <p role="alert">{error}</p> : null}
<footer>
<button
type="button"
disabled={!dirty || saving}
onClick={() => { void publish(session); }}
>{saving ? "Publishing…" : "Save & publish"}</button>
<button
type="button"
disabled={dirty || saving || nextCount === 0}
title={nextCount === 0 ? `No remaining ${target.kind} review targets.` : undefined}
onClick={() => enterRandomGmReview(target.kind)}
>Test another</button>
</footer>
</section>
);
}