forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
8a98797fcf
commit
11934a3979
44 files changed
+3183
-329
No files matched your search
@@ -3,6 +3,18 @@ import catalog from "../src/game/generated/dungeonLeaderboardCatalog.generated.j
|
||||
const dungeons = new Map(catalog.dungeons.map((entry) => [entry.id, entry]));
|
||||
const classes = new Set(catalog.classes.map((entry) => entry.id));
|
||||
|
||||
function storedFinalBossId(dungeonId, difficultyId, killedBossIds) {
|
||||
const difficulty = dungeons.get(dungeonId)?.difficulties.find((entry) => entry.id === difficultyId);
|
||||
if (!difficulty) return null;
|
||||
if (!difficulty.finalBossIds?.length) return difficulty.finalBossId || null;
|
||||
const defeated = difficulty.finalBossIds.filter((id) => killedBossIds.includes(id));
|
||||
// The earlier Deadmines ship cut defeated Greenskin before VanCleef.
|
||||
if (dungeonId === "bite-sized-deadmines-ship" && defeated.length > 1 && defeated.includes("creature-79336")) {
|
||||
return "creature-79336";
|
||||
}
|
||||
return defeated.length === 1 ? defeated[0] : null;
|
||||
}
|
||||
|
||||
export function createDungeonLeaderboardRepository(database, { ErrorType }) {
|
||||
database.exec(`
|
||||
CREATE TABLE IF NOT EXISTS dungeon_runs (
|
||||
@@ -26,12 +38,22 @@ export function createDungeonLeaderboardRepository(database, { ErrorType }) {
|
||||
if (!database.prepare("PRAGMA table_info(dungeon_runs)").all().some((column) => column.name === "boss_difficulty")) {
|
||||
database.exec("ALTER TABLE dungeon_runs ADD COLUMN boss_difficulty INTEGER NOT NULL DEFAULT 0 CHECK (boss_difficulty BETWEEN 0 AND 10)");
|
||||
}
|
||||
if (!database.prepare("PRAGMA table_info(dungeon_runs)").all().some((column) => column.name === "final_boss_id")) {
|
||||
database.exec("ALTER TABLE dungeon_runs ADD COLUMN final_boss_id TEXT");
|
||||
}
|
||||
const updateFinalBoss = database.prepare("UPDATE dungeon_runs SET final_boss_id = ? WHERE id = ?");
|
||||
for (const row of database.prepare("SELECT id, dungeon_id, difficulty_id, bosses_json FROM dungeon_runs WHERE final_boss_id IS NULL").all()) {
|
||||
const bossId = storedFinalBossId(row.dungeon_id, row.difficulty_id, JSON.parse(row.bosses_json));
|
||||
if (bossId) updateFinalBoss.run(bossId, row.id);
|
||||
}
|
||||
database.exec(`CREATE INDEX IF NOT EXISTS dungeon_runs_boss_difficulty_idx
|
||||
ON dungeon_runs(dungeon_id, difficulty_id, boss_difficulty, full_clear, duration_ms, finished_at, id)`);
|
||||
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 = ?");
|
||||
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)
|
||||
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)
|
||||
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.boss_difficulty > 0 ? { bossDifficulty: Number(row.boss_difficulty) } : {}),
|
||||
@@ -48,13 +70,21 @@ export function createDungeonLeaderboardRepository(database, { ErrorType }) {
|
||||
fail("Choose a boss difficulty from 1 to 10.");
|
||||
return { dungeon, difficultyId, difficulty, bossDifficulty };
|
||||
}
|
||||
function placement(run) {
|
||||
function boardBossId(difficulty, requestedBossId) {
|
||||
const finalBossIds = difficulty.finalBossIds ?? [difficulty.finalBossId];
|
||||
if (requestedBossId && !finalBossIds.includes(requestedBossId)) fail("Choose a final boss from this dungeon.");
|
||||
return difficulty.finalBossIds?.length ? requestedBossId || finalBossIds[0] : null;
|
||||
}
|
||||
function placement(run, selectedBossId = storedFinalBossId(run.dungeonId, run.difficultyId, run.killedBossIds)) {
|
||||
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 = ?`)
|
||||
.get(run.durationMs, run.durationMs, run.dungeonId, run.difficultyId, run.bossDifficulty ?? 0);
|
||||
FROM dungeon_runs WHERE dungeon_id = ? AND difficulty_id = ? AND boss_difficulty = ? ${bossCondition}`)
|
||||
.get(run.durationMs, run.durationMs, run.dungeonId, run.difficultyId, run.bossDifficulty ?? 0,
|
||||
...(variant ? [selectedBossId] : []));
|
||||
return { finalRank: Number(counts.faster) + 1, finalTotal: Number(counts.total),
|
||||
fullRank: run.fullClear ? Number(counts.full_faster) + 1 : null, fullTotal: Number(counts.full_total) };
|
||||
}
|
||||
@@ -74,6 +104,8 @@ export function createDungeonLeaderboardRepository(database, { ErrorType }) {
|
||||
const killedBossIds = [...new Set(input.killedBossIds)].sort();
|
||||
const finalBossIds = difficulty.finalBossIds ?? [difficulty.finalBossId];
|
||||
if (!finalBossIds.some((id) => killedBossIds.includes(id))) fail("The final boss must be defeated before recording a run.");
|
||||
if (difficulty.finalBossIds && finalBossIds.filter((id) => killedBossIds.includes(id)).length !== 1)
|
||||
fail("A randomized run must defeat exactly one selected final boss.");
|
||||
if (!Array.isArray(input.party) || input.party.length < 1 || input.party.length > 5) fail("Provide the run's party.");
|
||||
const party = input.party.map((member) => {
|
||||
if (!member || typeof member !== "object" || typeof member.name !== "string" || !member.name.trim()
|
||||
@@ -99,17 +131,18 @@ export function createDungeonLeaderboardRepository(database, { ErrorType }) {
|
||||
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,
|
||||
run.finishedAt, JSON.stringify(party), JSON.stringify(killedBossIds), account?.id ?? null, Date.now(), bossDifficulty);
|
||||
return { run, placement: placement(run) };
|
||||
run.finishedAt, JSON.stringify(party), JSON.stringify(killedBossIds), account?.id ?? null, Date.now(), bossDifficulty,
|
||||
storedFinalBossId(run.dungeonId, run.difficultyId, run.killedBossIds));
|
||||
return { run, placement: placement(run, existing?.final_boss_id ?? storedFinalBossId(run.dungeonId, run.difficultyId, run.killedBossIds)) };
|
||||
},
|
||||
getDungeonRun(id) {
|
||||
const row = byId.get(id);
|
||||
if (!row) throw new ErrorType("That dungeon run is not recorded yet.", 404, "run_not_found");
|
||||
const run = record(row);
|
||||
return { run, placement: placement(run) };
|
||||
return { run, placement: placement(run, row.final_boss_id) };
|
||||
},
|
||||
listDungeonLeaderboard(input) {
|
||||
const { dungeon, difficultyId, bossDifficulty } = selection(input);
|
||||
const { dungeon, difficultyId, difficulty, bossDifficulty } = 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);
|
||||
@@ -117,11 +150,14 @@ export function createDungeonLeaderboardRepository(database, { ErrorType }) {
|
||||
if (!Number.isSafeInteger(offset) || offset < 0 || offset > 1_000_000
|
||||
|| !Number.isSafeInteger(limit) || limit < 1 || limit > 100) fail("Choose a valid leaderboard page.");
|
||||
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 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}
|
||||
ORDER BY duration_ms, finished_at, id LIMIT ? OFFSET ?`).all(dungeon.id, difficultyId, bossDifficulty, limit, offset);
|
||||
WHERE dungeon_id = ? AND difficulty_id = ? AND boss_difficulty = ? ${condition} ${bossCondition}
|
||||
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}`).get(dungeon.id, difficultyId, bossDifficulty).total;
|
||||
WHERE dungeon_id = ? AND difficulty_id = ? AND boss_difficulty = ? ${condition} ${bossCondition}`).get(...args).total;
|
||||
return { entries: rows.map((row) => ({ ...record(row), rank: Number(row.place) })), total: Number(total) };
|
||||
},
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user