Files
healer-man/src/game/gmContentReview.ts
T
2026-09-01 14:35:56 -04:00

180 lines
5.8 KiB
TypeScript

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