Release Healer Man 0.1.12
This commit is contained in:
1 parent
13970d03c8
commit
65e24f644e
201 files changed
+5061
-123
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user