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

+44 -13
View File
@@ -1,4 +1,5 @@
import catalog from "../src/game/generated/dungeonLeaderboardCatalog.generated.json" with { type: "json" };
import { defaultBiteSizedBoardScope, normalizeBiteSizedSelection } from "../shared/bite-sized-layouts.mjs";
const dungeons = new Map(catalog.dungeons.map((entry) => [entry.id, entry]));
const classes = new Set(catalog.classes.map((entry) => entry.id));
@@ -15,7 +16,7 @@ function storedFinalBossId(dungeonId, difficultyId, killedBossIds) {
return defeated.length === 1 ? defeated[0] : null;
}
export function createDungeonLeaderboardRepository(database, { ErrorType }) {
export function createDungeonLeaderboardRepository(database, { ErrorType, biteSizedLayouts }) {
database.exec(`
CREATE TABLE IF NOT EXISTS dungeon_runs (
id TEXT PRIMARY KEY,
@@ -51,11 +52,20 @@ export function createDungeonLeaderboardRepository(database, { ErrorType }) {
database.exec(`CREATE INDEX IF NOT EXISTS dungeon_runs_final_boss_idx
ON dungeon_runs(dungeon_id, difficulty_id, boss_difficulty, final_boss_id, full_clear, duration_ms, finished_at, id)`);
const byId = database.prepare("SELECT * FROM dungeon_runs WHERE id = ?");
for (const [name, type] of [["spot_id", "TEXT"], ["spot_revision", "INTEGER"], ["selection_json", "TEXT"]]) {
if (!database.prepare("PRAGMA table_info(dungeon_runs)").all().some((c) => c.name === name)) database.exec(`ALTER TABLE dungeon_runs ADD COLUMN ${name} ${type}`);
}
for (const row of database.prepare("SELECT id, dungeon_id FROM dungeon_runs WHERE spot_id IS NULL").all()) {
const scope = defaultBiteSizedBoardScope(row.dungeon_id);
if (scope.spotId) database.prepare("UPDATE dungeon_runs SET spot_id = ?, spot_revision = ? WHERE id = ?").run(scope.spotId, scope.spotRevision, row.id);
}
database.exec("CREATE INDEX IF NOT EXISTS dungeon_runs_spot_times_idx ON dungeon_runs(dungeon_id, spot_id, spot_revision, difficulty_id, boss_difficulty, final_boss_id, duration_ms)");
const insert = database.prepare(`INSERT INTO dungeon_runs
(id, dungeon_id, difficulty_id, duration_ms, full_clear, finished_at, party_json, bosses_json, submitted_by_account_id, received_at, boss_difficulty, final_boss_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
(id, dungeon_id, difficulty_id, duration_ms, full_clear, finished_at, party_json, bosses_json, submitted_by_account_id, received_at, boss_difficulty, final_boss_id, spot_id, spot_revision, selection_json)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
const fail = (message) => { throw new ErrorType(message, 400, "invalid_dungeon_run"); };
const record = (row) => ({ id: row.id, dungeonId: row.dungeon_id, difficultyId: row.difficulty_id,
...(row.selection_json ? { biteSizedSelection: JSON.parse(row.selection_json) } : {}),
...(row.boss_difficulty > 0 ? { bossDifficulty: Number(row.boss_difficulty) } : {}),
durationMs: Number(row.duration_ms), finishedAt: Number(row.finished_at), fullClear: row.full_clear === 1,
party: JSON.parse(row.party_json), killedBossIds: JSON.parse(row.bosses_json) });
@@ -68,7 +78,15 @@ export function createDungeonLeaderboardRepository(database, { ErrorType }) {
const bossDifficulty = input.bossDifficulty ?? 0;
if (!Number.isInteger(bossDifficulty) || bossDifficulty < 0 || bossDifficulty > 10)
fail("Choose a boss difficulty from 1 to 10.");
return { dungeon, difficultyId, difficulty, bossDifficulty };
const defaults = defaultBiteSizedBoardScope(dungeon.id);
const spotId = input.spotId || defaults.spotId;
const spotRevision = input.spotRevision == null ? defaults.spotRevision : Number(input.spotRevision);
if (spotId && (!Number.isSafeInteger(spotRevision) || spotRevision < 0)) fail("Choose a valid spot revision.");
if (spotId && biteSizedLayouts) {
const spot = biteSizedLayouts.getBiteSizedSpotRevision(spotId, spotRevision);
if (!dungeon.id.startsWith("randomized-bite-sized-") && spot.dungeonId !== dungeon.id) fail("Choose a spot in this dungeon.");
}
return { dungeon, difficultyId, difficulty, bossDifficulty, spotId, spotRevision };
}
function boardBossId(difficulty, requestedBossId) {
const finalBossIds = difficulty.finalBossIds ?? [difficulty.finalBossId];
@@ -76,15 +94,17 @@ export function createDungeonLeaderboardRepository(database, { ErrorType }) {
return difficulty.finalBossIds?.length ? requestedBossId || finalBossIds[0] : null;
}
function placement(run, selectedBossId = storedFinalBossId(run.dungeonId, run.difficultyId, run.killedBossIds)) {
const scope = run.biteSizedSelection ? { spotId: run.biteSizedSelection.spot.id, spotRevision: run.biteSizedSelection.spot.revision } : defaultBiteSizedBoardScope(run.dungeonId);
const spotCondition = scope.spotId ? "AND spot_id = ? AND spot_revision = ?" : "";
const variant = dungeons.get(run.dungeonId)?.difficulties.find((entry) => entry.id === run.difficultyId)?.finalBossIds?.length;
const bossCondition = variant ? "AND final_boss_id = ?" : "";
const counts = database.prepare(`SELECT COUNT(*) AS total,
COALESCE(SUM(CASE WHEN duration_ms < ? THEN 1 ELSE 0 END), 0) AS faster,
COALESCE(SUM(full_clear), 0) AS full_total,
COALESCE(SUM(CASE WHEN full_clear = 1 AND duration_ms < ? THEN 1 ELSE 0 END), 0) AS full_faster
FROM dungeon_runs WHERE dungeon_id = ? AND difficulty_id = ? AND boss_difficulty = ? ${bossCondition}`)
FROM dungeon_runs WHERE dungeon_id = ? AND difficulty_id = ? AND boss_difficulty = ? ${bossCondition} ${spotCondition}`)
.get(run.durationMs, run.durationMs, run.dungeonId, run.difficultyId, run.bossDifficulty ?? 0,
...(variant ? [selectedBossId] : []));
...(variant ? [selectedBossId] : []), ...(scope.spotId ? [scope.spotId, scope.spotRevision] : []));
return { finalRank: Number(counts.faster) + 1, finalTotal: Number(counts.total),
fullRank: run.fullClear ? Number(counts.full_faster) + 1 : null, fullTotal: Number(counts.full_total) };
}
@@ -92,6 +112,12 @@ export function createDungeonLeaderboardRepository(database, { ErrorType }) {
submitDungeonRun(account, input) {
if (!input || typeof input !== "object" || Array.isArray(input)) fail("Provide a completed dungeon run.");
const { dungeon, difficultyId, difficulty, bossDifficulty } = selection(input);
let biteSizedSelection;
if (input.biteSizedSelection !== undefined) {
try { biteSizedSelection = biteSizedLayouts ? biteSizedLayouts.validateBiteSizedRunSelection(input.biteSizedSelection, dungeon.id) : normalizeBiteSizedSelection(input.biteSizedSelection); }
catch (error) { fail(error.message); }
if (biteSizedSelection.dungeonId !== dungeon.id || input.killedBossIds?.length !== 1 || input.killedBossIds[0] !== biteSizedSelection.boss.objectiveId) fail("Defeat the selected spot's boss.");
}
if (input.bossDifficulty !== undefined && bossDifficulty === 0) fail("Choose a boss difficulty from 1 to 10.");
if (!difficulty.finalBossId) fail("This dungeon does not have a configured final boss yet.");
if (typeof input.id !== "string" || !/^[A-Za-z0-9_-]{8,100}$/.test(input.id)) fail("Provide a valid run id.");
@@ -121,18 +147,22 @@ export function createDungeonLeaderboardRepository(database, { ErrorType }) {
? difficulty.finalBossIds.some((id) => killedBossIds.includes(id))
: difficulty.bossIds.every((id) => killedBossIds.includes(id));
const run = { id: input.id, dungeonId: dungeon.id, difficultyId, durationMs: input.durationMs,
...(biteSizedSelection ? { biteSizedSelection } : {}),
...(bossDifficulty > 0 ? { bossDifficulty } : {}),
finishedAt: input.finishedAt, killedBossIds, party, fullClear };
const existing = byId.get(run.id);
if (existing) {
if (JSON.stringify(record(existing)) !== JSON.stringify({ id: run.id, dungeonId: run.dungeonId,
difficultyId: run.difficultyId, ...(bossDifficulty > 0 ? { bossDifficulty } : {}),
difficultyId: run.difficultyId, ...(biteSizedSelection ? { biteSizedSelection } : {}), ...(bossDifficulty > 0 ? { bossDifficulty } : {}),
durationMs: run.durationMs, finishedAt: run.finishedAt,
fullClear: run.fullClear, party: run.party, killedBossIds: run.killedBossIds }))
throw new ErrorType("That run id already belongs to a different result.", 409, "run_conflict");
} else insert.run(run.id, run.dungeonId, run.difficultyId, run.durationMs, fullClear ? 1 : 0,
} else {
const scope = biteSizedSelection ? { spotId: biteSizedSelection.spot.id, spotRevision: biteSizedSelection.spot.revision } : defaultBiteSizedBoardScope(run.dungeonId);
insert.run(run.id, run.dungeonId, run.difficultyId, run.durationMs, fullClear ? 1 : 0,
run.finishedAt, JSON.stringify(party), JSON.stringify(killedBossIds), account?.id ?? null, Date.now(), bossDifficulty,
storedFinalBossId(run.dungeonId, run.difficultyId, run.killedBossIds));
storedFinalBossId(run.dungeonId, run.difficultyId, run.killedBossIds), scope.spotId, scope.spotRevision, biteSizedSelection ? JSON.stringify(biteSizedSelection) : null);
}
return { run, placement: placement(run, existing?.final_boss_id ?? storedFinalBossId(run.dungeonId, run.difficultyId, run.killedBossIds)) };
},
getDungeonRun(id) {
@@ -142,7 +172,7 @@ export function createDungeonLeaderboardRepository(database, { ErrorType }) {
return { run, placement: placement(run, row.final_boss_id) };
},
listDungeonLeaderboard(input) {
const { dungeon, difficultyId, difficulty, bossDifficulty } = selection(input);
const { dungeon, difficultyId, difficulty, bossDifficulty, spotId, spotRevision } = selection(input);
const filter = input.clear || "final";
if (!["full", "final"].includes(filter)) fail("Choose full dungeon clears or final boss clears.");
const offset = Number(input.offset ?? 0);
@@ -152,12 +182,13 @@ export function createDungeonLeaderboardRepository(database, { ErrorType }) {
const condition = filter === "full" ? "AND full_clear = 1" : "";
const bossId = boardBossId(difficulty, input.bossId);
const bossCondition = bossId ? "AND final_boss_id = ?" : "";
const args = [dungeon.id, difficultyId, bossDifficulty, ...(bossId ? [bossId] : [])];
const spotCondition = spotId ? "AND spot_id = ? AND spot_revision = ?" : "";
const args = [dungeon.id, difficultyId, bossDifficulty, ...(bossId ? [bossId] : []), ...(spotId ? [spotId, spotRevision] : [])];
const rows = database.prepare(`SELECT *, RANK() OVER (ORDER BY duration_ms) AS place FROM dungeon_runs
WHERE dungeon_id = ? AND difficulty_id = ? AND boss_difficulty = ? ${condition} ${bossCondition}
WHERE dungeon_id = ? AND difficulty_id = ? AND boss_difficulty = ? ${condition} ${bossCondition} ${spotCondition}
ORDER BY duration_ms, finished_at, id LIMIT ? OFFSET ?`).all(...args, limit, offset);
const total = database.prepare(`SELECT COUNT(*) AS total FROM dungeon_runs
WHERE dungeon_id = ? AND difficulty_id = ? AND boss_difficulty = ? ${condition} ${bossCondition}`).get(...args).total;
WHERE dungeon_id = ? AND difficulty_id = ? AND boss_difficulty = ? ${condition} ${bossCondition} ${spotCondition}`).get(...args).total;
return { entries: rows.map((row) => ({ ...record(row), rank: Number(row.place) })), total: Number(total) };
},
};