Update Healer Man server

This commit is contained in:
phenom committed 2026-09-27 21:48:00 -04:00
1 parent 15a5254380
commit 9d7e0e2f9e
71 files changed
+3490 -81

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
import { BITE_SIZED_DEFAULT_CONFIG, BITE_SIZED_LAYOUT_CATALOG, normalizeBiteSizedConfig, normalizeBiteSizedSelection, biteSizedGameplayKey, selectBiteSizedRun } from "../shared/bite-sized-layouts.mjs";
import { readFileSync } from "node:fs";
import { gunzipSync } from "node:zlib";
import { validateBiteSizedSpotNavigation } from "./generated/bite-sized-navigation.mjs";
const navigationGraphs = JSON.parse(gunzipSync(readFileSync(new URL("./generated/bite-sized-navigation.json.gz", import.meta.url))).toString("utf8"));
export function createBiteSizedLayoutRepository(database, { ErrorType, isGameMaster }) {
database.exec(`CREATE TABLE IF NOT EXISTS bite_sized_layout_config (
id INTEGER PRIMARY KEY CHECK(id = 1), revision INTEGER NOT NULL, updated_at INTEGER, config_json TEXT NOT NULL
) STRICT;
CREATE TABLE IF NOT EXISTS bite_sized_spot_revisions (
spot_id TEXT NOT NULL, revision INTEGER NOT NULL, spot_json TEXT NOT NULL, PRIMARY KEY(spot_id, revision)
) STRICT;`);
database.prepare("INSERT OR IGNORE INTO bite_sized_layout_config VALUES (1, 0, NULL, ?)").run(JSON.stringify(normalizeBiteSizedConfig(BITE_SIZED_DEFAULT_CONFIG)));
const archive = database.prepare("INSERT OR IGNORE INTO bite_sized_spot_revisions VALUES (?, ?, ?)");
for (const spot of normalizeBiteSizedConfig(BITE_SIZED_DEFAULT_CONFIG).spots) archive.run(spot.id, spot.revision, JSON.stringify(spot));
function getBiteSizedLayouts() {
const row = database.prepare("SELECT * FROM bite_sized_layout_config WHERE id = 1").get();
return { revision: row.revision, updatedAt: row.updated_at, config: normalizeBiteSizedConfig(JSON.parse(row.config_json)) };
}
function getBiteSizedSpotRevision(id, revision) {
const row = database.prepare("SELECT spot_json FROM bite_sized_spot_revisions WHERE spot_id = ? AND revision = ?").get(id, revision);
if (!row) throw new ErrorType("That spot revision is unavailable.", 404, "spot_not_found");
return JSON.parse(row.spot_json);
}
function listBiteSizedSpotRevisions() {
return database.prepare("SELECT spot_json FROM bite_sized_spot_revisions ORDER BY spot_id, revision DESC").all().map((row) => JSON.parse(row.spot_json));
}
function putBiteSizedLayouts(account, expectedRevision, input) {
if (!isGameMaster(account)) throw new ErrorType("Game Master access is required.", 403, "gm_required");
let config;
try { config = normalizeBiteSizedConfig(input); }
catch (error) { throw new ErrorType(error.message, 400, "invalid_bite_sized_layout"); }
database.exec("BEGIN IMMEDIATE");
try {
const current = getBiteSizedLayouts();
if (!Number.isSafeInteger(expectedRevision) || expectedRevision !== current.revision) throw new ErrorType("Layouts changed on the server. Refresh before publishing; your draft is still available.", 409, "revision_conflict");
for (const spot of config.spots) {
const previous = current.config.spots.find((s) => s.id === spot.id);
if (previous && biteSizedGameplayKey(previous) === biteSizedGameplayKey(spot)) continue;
try {
const graph = navigationGraphs[spot.sourceDungeonId];
const blockers = spot.legacyLayout ? BITE_SIZED_LAYOUT_CATALOG.dungeons.find((d) => d.id === spot.dungeonId).legacyBlockers : [];
validateBiteSizedSpotNavigation(spot, graph, spot.legacyLayout ? graph.legacyRegions ?? graph.regions : graph.regions, blockers);
}
catch (error) { throw new ErrorType(error.message, 400, "invalid_bite_sized_navigation"); }
}
const spots = config.spots.map((spot) => {
const previous = current.config.spots.find((s) => s.id === spot.id);
const latest = database.prepare("SELECT MAX(revision) AS revision FROM bite_sized_spot_revisions WHERE spot_id = ?").get(spot.id).revision;
const revision = previous && biteSizedGameplayKey(previous) === biteSizedGameplayKey(spot) ? previous.revision : (latest ?? -1) + 1;
const next = { ...spot, revision };
archive.run(next.id, revision, JSON.stringify(next));
return next;
});
const next = { schemaVersion: 1, spots };
database.prepare("UPDATE bite_sized_layout_config SET revision = revision + 1, updated_at = ?, config_json = ? WHERE id = 1").run(Date.now(), JSON.stringify(next));
database.exec("COMMIT");
return getBiteSizedLayouts();
} catch (error) { database.exec("ROLLBACK"); throw error; }
}
function resolveBiteSizedRun(dungeonId, seed, eligibleDungeonIds) {
return selectBiteSizedRun(getBiteSizedLayouts().config, dungeonId, seed, { eligibleDungeonIds });
}
function validateBiteSizedRunSelection(input, dungeonId) {
let selection;
try { selection = normalizeBiteSizedSelection(input); }
catch (error) { throw new ErrorType(error.message, 400, "invalid_bite_sized_selection"); }
const archived = getBiteSizedSpotRevision(selection.spot.id, selection.spot.revision);
if (selection.dungeonId !== dungeonId || biteSizedGameplayKey(archived) !== biteSizedGameplayKey(selection.spot)) {
throw new ErrorType("The run does not match its published spot revision.", 400, "invalid_bite_sized_selection");
}
return selection;
}
return { getBiteSizedLayouts, getBiteSizedSpotRevision, listBiteSizedSpotRevisions, putBiteSizedLayouts, resolveBiteSizedRun, validateBiteSizedRunSelection };
}