180 lines
5.8 KiB
TypeScript
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 },
|
|
},
|
|
};
|
|
}
|