Files
healer-man/server/manastorm-config.mjs
T
2026-08-18 16:22:19 -04:00

162 lines
6.1 KiB
JavaScript

export const MANASTORM_ADMIN_CONFIG_SCHEMA_VERSION = 1;
export const DEFAULT_MANASTORM_ADMIN_CONFIG = Object.freeze({
schemaVersion: MANASTORM_ADMIN_CONFIG_SCHEMA_VERSION,
global: Object.freeze({
enabled: true,
levelingEnabled: true,
endgameEnabled: true,
affixesEnabled: true,
chaoticLinkEnabled: true,
milestoneEncountersEnabled: true,
}),
dungeonSpawns: Object.freeze({}),
bosses: Object.freeze({}),
});
const BOSS_STATUSES = new Set(["ready", "needs-tested", "not-ready"]);
const MAX_RECORDS = 10_000;
const MAX_ID_LENGTH = 256;
function invalid(message) {
const error = new Error(message);
error.code = "invalid_manastorm_config";
return error;
}
function record(value, label) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw invalid(`${label} must be an object.`);
}
return value;
}
function identifier(value, label) {
if (typeof value !== "string" || !value.trim() || value.length > MAX_ID_LENGTH) {
throw invalid(`${label} must be a non-empty string no longer than ${MAX_ID_LENGTH} characters.`);
}
return value.trim();
}
function optionalIdentifier(value, label) {
return value === undefined || value === null ? undefined : identifier(value, label);
}
function finiteNumber(value, label) {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw invalid(`${label} must be a finite number.`);
}
return value;
}
function vector(value, label) {
if (!Array.isArray(value) || value.length !== 3) {
throw invalid(`${label} must contain exactly three coordinates.`);
}
return value.map((coordinate, index) => finiteNumber(coordinate, `${label}[${index}]`));
}
function worldSpawn(value, label) {
const input = record(value, label);
return {
footPosition: vector(input.footPosition, `${label}.footPosition`),
yaw: finiteNumber(input.yaw, `${label}.yaw`),
};
}
function boolean(value, label) {
if (typeof value !== "boolean") throw invalid(`${label} must be a boolean.`);
return value;
}
function normalizeGlobal(value) {
const input = record(value, "config.global");
return {
enabled: boolean(input.enabled, "config.global.enabled"),
levelingEnabled: boolean(input.levelingEnabled, "config.global.levelingEnabled"),
endgameEnabled: boolean(input.endgameEnabled, "config.global.endgameEnabled"),
affixesEnabled: boolean(input.affixesEnabled, "config.global.affixesEnabled"),
chaoticLinkEnabled: boolean(input.chaoticLinkEnabled, "config.global.chaoticLinkEnabled"),
milestoneEncountersEnabled: boolean(
input.milestoneEncountersEnabled,
"config.global.milestoneEncountersEnabled",
),
};
}
function normalizeStage(value, label) {
const input = record(value, label);
const linkedMobRuntimeIds = Array.isArray(input.linkedMobRuntimeIds)
? input.linkedMobRuntimeIds.map((id, index) => identifier(id, `${label}.linkedMobRuntimeIds[${index}]`))
: (() => { throw invalid(`${label}.linkedMobRuntimeIds must be an array.`); })();
if (new Set(linkedMobRuntimeIds).size !== linkedMobRuntimeIds.length) {
throw invalid(`${label}.linkedMobRuntimeIds must not contain duplicates.`);
}
const requiredKillCount = finiteNumber(input.requiredKillCount, `${label}.requiredKillCount`);
if (!Number.isInteger(requiredKillCount) || requiredKillCount < 0 || requiredKillCount > linkedMobRuntimeIds.length) {
throw invalid(`${label}.requiredKillCount must be an integer between zero and the linked mob count.`);
}
const mapId = finiteNumber(input.mapId, `${label}.mapId`);
if (!Number.isInteger(mapId) || mapId <= 0) throw invalid(`${label}.mapId must be a positive integer.`);
return {
dungeonId: identifier(input.dungeonId, `${label}.dungeonId`),
mapId,
bossRuntimeId: identifier(input.bossRuntimeId, `${label}.bossRuntimeId`),
bossEntityId: identifier(input.bossEntityId, `${label}.bossEntityId`),
bossName: identifier(input.bossName, `${label}.bossName`),
...(optionalIdentifier(input.baseStageId, `${label}.baseStageId`)
? { baseStageId: optionalIdentifier(input.baseStageId, `${label}.baseStageId`) }
: {}),
...(optionalIdentifier(input.assetPackageId, `${label}.assetPackageId`)
? { assetPackageId: optionalIdentifier(input.assetPackageId, `${label}.assetPackageId`) }
: {}),
bossPosition: vector(input.bossPosition, `${label}.bossPosition`),
bossYaw: finiteNumber(input.bossYaw, `${label}.bossYaw`),
groupSpawn: worldSpawn(input.groupSpawn, `${label}.groupSpawn`),
portalPosition: vector(input.portalPosition, `${label}.portalPosition`),
linkedMobRuntimeIds,
requiredKillCount,
};
}
function normalizeDictionary(value, label, normalizeValue) {
const input = record(value, label);
const entries = Object.entries(input);
if (entries.length > MAX_RECORDS) throw invalid(`${label} contains too many records.`);
return Object.fromEntries(entries.map(([key, entry]) => [
identifier(key, `${label} key`),
normalizeValue(entry, `${label}.${key}`),
]));
}
export function normalizeManastormAdminConfig(value) {
const input = record(value, "config");
if (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 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 (boss.status === "ready" && (boss.stage === undefined || boss.stage === null)) {
throw invalid(`${label} cannot be Ready without a complete stage.`);
}
return {
status: boss.status,
...(boss.stage === undefined || boss.stage === null
? {}
: { stage: normalizeStage(boss.stage, `${label}.stage`) }),
};
});
return {
schemaVersion: MANASTORM_ADMIN_CONFIG_SCHEMA_VERSION,
global: normalizeGlobal(input.global),
dungeonSpawns,
bosses,
};
}
export function cloneDefaultManastormAdminConfig() {
return JSON.parse(JSON.stringify(DEFAULT_MANASTORM_ADMIN_CONFIG));
}