Update Healer Man server

This commit is contained in:
phenom committed 2026-09-26 22:02:18 -04:00
1 parent 8a98797fcf
commit 11934a3979
44 files changed
+3183 -329

No files matched your search

+37 -10
View File
@@ -1,8 +1,15 @@
import catalog from "../src/game/generated/dungeonLeaderboardCatalog.generated.json" with { type: "json" };
import { RACE_PERIOD_MS, racePoolForDungeon } from "./dungeon-race-rules.mjs";
import { hashRaceSeed, RACE_PERIOD_MS, racePoolForDungeon } from "./dungeon-race-rules.mjs";
const dungeons = new Map(catalog.dungeons.map((entry) => [entry.id, entry]));
function raceBossId(dungeonId, difficultyId, seed) {
const difficulty = dungeons.get(dungeonId)?.difficulties.find((entry) => entry.id === difficultyId);
const finalIds = difficulty?.finalBossIds;
const objectiveSeed = dungeonId.startsWith("randomized-bite-sized-") ? `${seed}:boss` : seed;
return finalIds?.length ? finalIds[hashRaceSeed(objectiveSeed) % finalIds.length] : null;
}
export function createDungeonRaceRepository(database, { ErrorType }) {
database.exec(`
CREATE TABLE IF NOT EXISTS dungeon_race_matches (
@@ -26,13 +33,24 @@ export function createDungeonRaceRepository(database, { ErrorType }) {
CREATE INDEX IF NOT EXISTS dungeon_race_account_idx
ON dungeon_race_participants(account_id, outcome, clear_at);
`);
if (!database.prepare("PRAGMA table_info(dungeon_race_matches)").all().some((column) => column.name === "boss_id")) {
database.exec("ALTER TABLE dungeon_race_matches ADD COLUMN boss_id TEXT");
}
const missingBosses = database.prepare("SELECT id, dungeon_id, difficulty_id, seed FROM dungeon_race_matches WHERE boss_id IS NULL").all();
const updateBoss = database.prepare("UPDATE dungeon_race_matches SET boss_id = ? WHERE id = ?");
for (const match of missingBosses) {
const bossId = raceBossId(match.dungeon_id, match.difficulty_id, match.seed);
if (bossId) updateBoss.run(bossId, match.id);
}
database.exec(`CREATE INDEX IF NOT EXISTS dungeon_race_boss_idx
ON dungeon_race_matches(dungeon_id, boss_id, status, settled_at)`);
database.prepare("UPDATE dungeon_race_matches SET status = 'void', settled_at = ? WHERE status IN ('preparing', 'active')")
.run(Date.now());
const matchById = database.prepare("SELECT * FROM dungeon_race_matches WHERE id = ?");
const participantsById = database.prepare("SELECT * FROM dungeon_race_participants WHERE match_id = ? ORDER BY side");
const insertMatch = database.prepare(`INSERT INTO dungeon_race_matches
(id, dungeon_id, seed, difficulty_id, boss_difficulty, status, created_at, bot_json)
VALUES (?, ?, ?, ?, 1, 'preparing', ?, ?)`);
(id, dungeon_id, seed, difficulty_id, boss_difficulty, status, created_at, bot_json, boss_id)
VALUES (?, ?, ?, ?, 1, 'preparing', ?, ?, ?)`);
const insertParticipant = database.prepare(`INSERT INTO dungeon_race_participants
(match_id, side, account_id, character_id, username, source, role)
VALUES (?, ?, ?, ?, ?, ?, ?)`);
@@ -72,7 +90,7 @@ export function createDungeonRaceRepository(database, { ErrorType }) {
function createMatch(match) {
transaction(() => {
insertMatch.run(match.id, match.dungeonId, match.seed, match.difficultyId, match.createdAt,
match.bot ? JSON.stringify(match.bot) : null);
match.bot ? JSON.stringify(match.bot) : null, raceBossId(match.dungeonId, match.difficultyId, match.seed));
for (const participant of match.participants) insertParticipant.run(match.id, participant.side,
participant.accountId, participant.characterId, participant.username, participant.source, participant.role);
});
@@ -95,8 +113,10 @@ export function createDungeonRaceRepository(database, { ErrorType }) {
|| match.status === "void") throw new ErrorType("This race is not accepting clears.", 409, "race_not_active");
const difficulty = dungeons.get(match.dungeonId)?.difficulties.find((entry) => entry.id === match.difficultyId);
const finalIds = difficulty?.finalBossIds ?? (difficulty?.finalBossId ? [difficulty.finalBossId] : []);
const selectedBossId = raceBossId(match.dungeonId, match.difficultyId, match.seed);
if (!Array.isArray(bossIds) || bossIds.length > 100 || bossIds.some((value) => !difficulty?.bossIds.includes(value))
|| !bossIds.some((value) => finalIds.includes(value)))
|| !bossIds.some((value) => finalIds.includes(value))
|| (selectedBossId && (bossIds.length !== 1 || bossIds[0] !== selectedBossId)))
throw new ErrorType("Defeat the race's final boss before reporting a clear.", 400, "invalid_race_clear");
return transaction(() => {
updateClear.run(timestamp, Math.max(1, timestamp - match.startAt), id, accountId);
@@ -120,28 +140,34 @@ export function createDungeonRaceRepository(database, { ErrorType }) {
.get(accountId, timestamp - RACE_PERIOD_MS);
return { wins: Number(row.wins ?? 0), losses: Number(row.losses ?? 0) };
}
function leaderboard({ view = "wins", period = "30d", dungeonId = null, limit = 20, offset = 0 } = {}, timestamp = Date.now()) {
function leaderboard({ view = "wins", period = "30d", dungeonId = null, bossId = null,
limit = 20, offset = 0 } = {}, timestamp = Date.now()) {
const dungeon = dungeons.get(dungeonId);
const bossIds = dungeon?.difficulties.find((entry) => entry.id === dungeon.defaultDifficultyId)?.finalBossIds;
if (!["wins", "times"].includes(view) || !["30d", "all"].includes(period)
|| (dungeonId !== null && (!dungeons.has(dungeonId) || !racePoolForDungeon(dungeonId)))
|| (bossId !== null && (!bossIds || !bossIds.includes(bossId)))
|| !Number.isSafeInteger(limit) || limit < 1 || limit > 100
|| !Number.isSafeInteger(offset) || offset < 0 || offset > 1_000_000)
throw new ErrorType("Choose a valid race leaderboard.", 400, "invalid_race_leaderboard");
if (view === "times" && !dungeonId) throw new ErrorType("Choose a dungeon for race times.", 400, "race_dungeon_required");
const since = period === "30d" ? timestamp - RACE_PERIOD_MS : 0;
const byDungeon = dungeonId ? "AND m.dungeon_id = ?" : "";
const args = dungeonId ? [since, dungeonId] : [since];
const selectedBossId = dungeonId && bossIds?.length ? bossId ?? bossIds[0] : null;
const byBoss = selectedBossId ? "AND m.boss_id = ?" : "";
const args = [since, ...(dungeonId ? [dungeonId] : []), ...(selectedBossId ? [selectedBossId] : [])];
const query = view === "wins" ? `
SELECT p.account_id AS accountId, a.username AS username,
SUM(CASE WHEN p.outcome = 'win' THEN 1 ELSE 0 END) AS wins,
SUM(CASE WHEN p.outcome = 'loss' THEN 1 ELSE 0 END) AS losses
FROM dungeon_race_participants p JOIN dungeon_race_matches m ON m.id = p.match_id
JOIN accounts a ON a.id = p.account_id
WHERE m.status = 'settled' AND m.settled_at >= ? ${byDungeon}
WHERE m.status = 'settled' AND m.settled_at >= ? ${byDungeon} ${byBoss}
GROUP BY p.account_id, a.username ORDER BY wins DESC, losses ASC, a.username COLLATE NOCASE ASC`
: `SELECT p.account_id AS accountId, a.username AS username, MIN(p.duration_ms) AS durationMs
FROM dungeon_race_participants p JOIN dungeon_race_matches m ON m.id = p.match_id
JOIN accounts a ON a.id = p.account_id
WHERE p.clear_at >= ? ${byDungeon} AND p.duration_ms IS NOT NULL
WHERE p.clear_at >= ? ${byDungeon} ${byBoss} AND p.duration_ms IS NOT NULL
GROUP BY p.account_id, a.username ORDER BY durationMs ASC, a.username COLLATE NOCASE ASC`;
const all = database.prepare(query).all(...args);
let lastValue = null;
@@ -154,7 +180,8 @@ export function createDungeonRaceRepository(database, { ErrorType }) {
? { wins: Number(row.wins), losses: Number(row.losses) }
: { durationMs: Number(row.durationMs) }) };
});
return { view, period, dungeonId, total: ranked.length, entries: ranked.slice(offset, offset + limit) };
return { view, period, dungeonId, bossId: selectedBossId,
total: ranked.length, entries: ranked.slice(offset, offset + limit) };
}
return { getMatch, createMatch, ready: (id, accountId, at) => (updateReady.run(at, id, accountId), getMatch(id)),
start: (id, at) => (updateStart.run(at, id), getMatch(id)), settleMatch, completeRaceRun,