import { DUNGEON_CATALOG, dungeonById } from "./dungeonCatalog"; import { bossAdminRecordForStatus, GM_REVIEW_NOTE_MAX_LENGTH, resolvedBossAdminCatalog, type GmReadinessStatus, type ManastormAdminConfig, type ResolvedBossAdminEntry, } from "./manastormAdminConfig"; export type GmReviewKind = "dungeon" | "boss" | "manastorm"; export type GmReviewTarget = | { readonly kind: "dungeon"; readonly dungeonId: string } | { readonly kind: "boss" | "manastorm"; readonly bossKey: string }; export interface GmReviewDescriptor { readonly target: GmReviewTarget; readonly name: string; readonly dungeonTitle: string; readonly mapId: number; readonly status: GmReadinessStatus; readonly note: string; readonly reviewedAt: number | null; readonly reviewedBy: string | null; readonly runnable: boolean; } function sameTarget(left: GmReviewTarget, right: GmReviewTarget): boolean { if (left.kind !== right.kind) return false; if (left.kind === "dungeon" && right.kind === "dungeon") return left.dungeonId === right.dungeonId; return left.kind !== "dungeon" && right.kind !== "dungeon" && left.bossKey === right.bossKey; } function bossTargets( kind: "boss" | "manastorm", config: ManastormAdminConfig, ): readonly GmReviewTarget[] { const source = kind === "boss" ? "dungeon" : "installed-manastorm"; return resolvedBossAdminCatalog(config) .filter((entry) => entry.runnable && entry.source.includes(source)) .map((entry) => ({ kind, bossKey: entry.key })); } function targetStatus(target: GmReviewTarget, config: ManastormAdminConfig): GmReadinessStatus { if (target.kind === "dungeon") return config.dungeons[target.dungeonId]?.status ?? "needs-tested"; return config.bosses[target.bossKey]?.status ?? "needs-tested"; } export function gmReviewQueue( kind: GmReviewKind, config: ManastormAdminConfig, previous: GmReviewTarget | null = null, ): readonly GmReviewTarget[] { const targets: readonly GmReviewTarget[] = kind === "dungeon" ? DUNGEON_CATALOG.filter((dungeon) => dungeon.available).map((dungeon) => ({ kind: "dungeon", dungeonId: dungeon.id, })) : bossTargets(kind, config); const needsTested = targets.filter((target) => targetStatus(target, config) === "needs-tested"); const prioritized = needsTested.length ? needsTested : targets.filter((target) => targetStatus(target, config) === "not-ready"); if (!previous || prioritized.length < 2) return prioritized; return prioritized.filter((target) => !sameTarget(target, previous)); } export function drawRandomGmReviewTarget( kind: GmReviewKind, config: ManastormAdminConfig, previous: GmReviewTarget | null = null, random: () => number = Math.random, ): GmReviewTarget | null { const queue = gmReviewQueue(kind, config, previous); if (!queue.length) return null; const draw = random(); const normalized = Number.isFinite(draw) ? Math.max(0, Math.min(0.999999999, draw)) : 0; return queue[Math.floor(normalized * queue.length)] ?? queue[0] ?? null; } function resolvedBoss(target: GmReviewTarget, config: ManastormAdminConfig): ResolvedBossAdminEntry | null { if (target.kind === "dungeon") return null; return resolvedBossAdminCatalog(config).find((entry) => entry.key === target.bossKey) ?? null; } export function describeGmReviewTarget( target: GmReviewTarget, config: ManastormAdminConfig, ): GmReviewDescriptor | null { if (target.kind === "dungeon") { const dungeon = dungeonById(target.dungeonId); if (!dungeon) return null; const record = config.dungeons[target.dungeonId]; return { target, name: dungeon.title, dungeonTitle: dungeon.location, mapId: dungeon.mapId, status: record?.status ?? "needs-tested", note: record?.note ?? "", reviewedAt: record?.reviewedAt ?? null, reviewedBy: record?.reviewedBy ?? null, runnable: dungeon.available, }; } const entry = resolvedBoss(target, config); if (!entry) return null; return { target, name: entry.name, dungeonTitle: entry.dungeonTitle, mapId: entry.mapId, status: entry.status, note: entry.reviewNote, reviewedAt: entry.reviewedAt, reviewedBy: entry.reviewedBy, runnable: entry.runnable, }; } export function configWithGmReviewStatus( config: ManastormAdminConfig, target: GmReviewTarget, status: GmReadinessStatus, ): ManastormAdminConfig | null { if (target.kind === "dungeon") { if (!dungeonById(target.dungeonId)) return null; return { ...config, dungeons: { ...config.dungeons, [target.dungeonId]: { ...config.dungeons[target.dungeonId], status }, }, }; } const entry = resolvedBoss(target, config); if (!entry || (status === "ready" && !entry.runnable)) return null; return { ...config, bosses: { ...config.bosses, [target.bossKey]: bossAdminRecordForStatus(entry, config, status), }, }; } export function configWithGmReviewNote( config: ManastormAdminConfig, target: GmReviewTarget, note: string, ): ManastormAdminConfig | null { const normalized = note.replace(/\r\n?/g, "\n").slice(0, GM_REVIEW_NOTE_MAX_LENGTH); const noteFields = normalized.trim() ? { note: normalized } : {}; if (target.kind === "dungeon") { if (!dungeonById(target.dungeonId)) return null; const record = { ...(config.dungeons[target.dungeonId] ?? { status: "needs-tested" as const }) }; delete record.note; return { ...config, dungeons: { ...config.dungeons, [target.dungeonId]: { ...record, ...noteFields }, }, }; } const entry = resolvedBoss(target, config); if (!entry) return null; const current = bossAdminRecordForStatus(entry, config, entry.status); const record = { ...current }; delete record.note; return { ...config, bosses: { ...config.bosses, [target.bossKey]: { ...record, ...noteFields }, }, }; }