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

+80 -4
View File
@@ -1,4 +1,5 @@
export const MANASTORM_ADMIN_CONFIG_SCHEMA_VERSION = 1;
export const MANASTORM_ADMIN_CONFIG_SCHEMA_VERSION = 2;
export const GM_REVIEW_NOTE_MAX_LENGTH = 1_000;
export const DEFAULT_MANASTORM_ADMIN_CONFIG = Object.freeze({
schemaVersion: MANASTORM_ADMIN_CONFIG_SCHEMA_VERSION,
@@ -11,10 +12,11 @@ export const DEFAULT_MANASTORM_ADMIN_CONFIG = Object.freeze({
milestoneEncountersEnabled: true,
}),
dungeonSpawns: Object.freeze({}),
dungeons: Object.freeze({}),
bosses: Object.freeze({}),
});
const BOSS_STATUSES = new Set(["ready", "needs-tested", "not-ready"]);
const READINESS_STATUSES = new Set(["ready", "needs-tested", "not-ready"]);
const MAX_RECORDS = 10_000;
const MAX_ID_LENGTH = 256;
@@ -42,6 +44,27 @@ function optionalIdentifier(value, label) {
return value === undefined || value === null ? undefined : identifier(value, label);
}
function reviewFields(value, label) {
const fields = {};
if (value.note !== undefined && value.note !== null) {
if (typeof value.note !== "string" || value.note.length > GM_REVIEW_NOTE_MAX_LENGTH) {
throw invalid(`${label}.note must be a string no longer than ${GM_REVIEW_NOTE_MAX_LENGTH} characters.`);
}
const note = value.note.replace(/\r\n?/g, "\n").trim();
if (note) fields.note = note;
}
if (value.reviewedAt !== undefined && value.reviewedAt !== null) {
if (!Number.isInteger(value.reviewedAt) || value.reviewedAt <= 0) {
throw invalid(`${label}.reviewedAt must be a positive integer timestamp.`);
}
fields.reviewedAt = value.reviewedAt;
}
if (value.reviewedBy !== undefined && value.reviewedBy !== null) {
fields.reviewedBy = identifier(value.reviewedBy, `${label}.reviewedBy`);
}
return fields;
}
function finiteNumber(value, label) {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw invalid(`${label} must be a finite number.`);
@@ -131,18 +154,26 @@ function normalizeDictionary(value, label, normalizeValue) {
export function normalizeManastormAdminConfig(value) {
const input = record(value, "config");
if (input.schemaVersion !== MANASTORM_ADMIN_CONFIG_SCHEMA_VERSION) {
if (input.schemaVersion !== 1 && input.schemaVersion !== MANASTORM_ADMIN_CONFIG_SCHEMA_VERSION) {
throw invalid(`Unsupported Manastorm admin config schema ${String(input.schemaVersion)}.`);
}
const dungeonSpawns = normalizeDictionary(input.dungeonSpawns, "config.dungeonSpawns", worldSpawn);
const dungeons = input.dungeons === undefined
? {}
: normalizeDictionary(input.dungeons, "config.dungeons", (rawDungeon, label) => {
const dungeon = record(rawDungeon, label);
if (!READINESS_STATUSES.has(dungeon.status)) throw invalid(`${label}.status is invalid.`);
return { status: dungeon.status, ...reviewFields(dungeon, label) };
});
const bosses = normalizeDictionary(input.bosses, "config.bosses", (rawBoss, label) => {
const boss = record(rawBoss, label);
if (!BOSS_STATUSES.has(boss.status)) throw invalid(`${label}.status is invalid.`);
if (!READINESS_STATUSES.has(boss.status)) throw invalid(`${label}.status is invalid.`);
if (boss.status === "ready" && (boss.stage === undefined || boss.stage === null)) {
throw invalid(`${label} cannot be Ready without a complete stage.`);
}
return {
status: boss.status,
...reviewFields(boss, label),
...(boss.stage === undefined || boss.stage === null
? {}
: { stage: normalizeStage(boss.stage, `${label}.stage`) }),
@@ -152,10 +183,55 @@ export function normalizeManastormAdminConfig(value) {
schemaVersion: MANASTORM_ADMIN_CONFIG_SCHEMA_VERSION,
global: normalizeGlobal(input.global),
dungeonSpawns,
dungeons,
bosses,
};
}
export function publicManastormAdminConfig(config) {
return {
...config,
dungeons: Object.fromEntries(Object.entries(config.dungeons).map(([key, record]) => [
key,
{ status: record.status },
])),
bosses: Object.fromEntries(Object.entries(config.bosses).map(([key, record]) => [
key,
{ status: record.status, ...(record.stage ? { stage: record.stage } : {}) },
])),
};
}
function stampedReviewRecord(previous, next, reviewedBy, reviewedAt) {
const changed = (previous?.status ?? "needs-tested") !== next.status
|| (previous?.note ?? "") !== (next.note ?? "");
const { reviewedAt: _incomingReviewedAt, reviewedBy: _incomingReviewedBy, ...record } = next;
if (changed) return { ...record, reviewedAt, reviewedBy };
return {
...record,
...(previous?.reviewedAt ? { reviewedAt: previous.reviewedAt } : {}),
...(previous?.reviewedBy ? { reviewedBy: previous.reviewedBy } : {}),
};
}
export function stampManastormReviewMetadata(previous, next, reviewedBy, reviewedAt) {
const reviewer = identifier(reviewedBy, "reviewedBy");
if (!Number.isInteger(reviewedAt) || reviewedAt <= 0) {
throw invalid("reviewedAt must be a positive integer timestamp.");
}
return {
...next,
dungeons: Object.fromEntries(Object.entries(next.dungeons).map(([key, record]) => [
key,
stampedReviewRecord(previous.dungeons[key], record, reviewer, reviewedAt),
])),
bosses: Object.fromEntries(Object.entries(next.bosses).map(([key, record]) => [
key,
stampedReviewRecord(previous.bosses[key], record, reviewer, reviewedAt),
])),
};
}
export function cloneDefaultManastormAdminConfig() {
return JSON.parse(JSON.stringify(DEFAULT_MANASTORM_ADMIN_CONFIG));
}