Update Healer Man server
This commit is contained in:
1 parent
15a5254380
commit
9d7e0e2f9e
71 files changed
+3490
-81
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import catalog from "../src/game/generated/dungeonLeaderboardCatalog.generated.json" with { type: "json" };
|
||||
import { hashRaceSeed, RACE_PERIOD_MS, racePoolForDungeon } from "./dungeon-race-rules.mjs";
|
||||
import { normalizeRaceCombatLog } from "./dungeon-race-combat-log.mjs";
|
||||
import { defaultBiteSizedBoardScope } from "../shared/bite-sized-layouts.mjs";
|
||||
|
||||
const dungeons = new Map(catalog.dungeons.map((entry) => [entry.id, entry]));
|
||||
|
||||
@@ -11,7 +12,7 @@ function raceBossId(dungeonId, difficultyId, seed) {
|
||||
return finalIds?.length ? finalIds[hashRaceSeed(objectiveSeed) % finalIds.length] : null;
|
||||
}
|
||||
|
||||
export function createDungeonRaceRepository(database, { ErrorType, isGameMaster = () => false }) {
|
||||
export function createDungeonRaceRepository(database, { ErrorType, isGameMaster = () => false, biteSizedLayouts }) {
|
||||
database.exec(`
|
||||
CREATE TABLE IF NOT EXISTS dungeon_race_matches (
|
||||
id TEXT PRIMARY KEY, dungeon_id TEXT NOT NULL, seed TEXT NOT NULL,
|
||||
@@ -64,6 +65,14 @@ export function createDungeonRaceRepository(database, { ErrorType, isGameMaster
|
||||
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 = ?");
|
||||
for (const [name, type] of [["spot_id", "TEXT"], ["spot_revision", "INTEGER"], ["selection_json", "TEXT"]]) {
|
||||
if (!database.prepare("PRAGMA table_info(dungeon_race_matches)").all().some((c) => c.name === name)) database.exec(`ALTER TABLE dungeon_race_matches ADD COLUMN ${name} ${type}`);
|
||||
}
|
||||
for (const row of database.prepare("SELECT id, dungeon_id FROM dungeon_race_matches WHERE spot_id IS NULL").all()) {
|
||||
const scope = defaultBiteSizedBoardScope(row.dungeon_id);
|
||||
if (scope.spotId) database.prepare("UPDATE dungeon_race_matches SET spot_id = ?, spot_revision = ? WHERE id = ?").run(scope.spotId, scope.spotRevision, row.id);
|
||||
}
|
||||
database.exec("CREATE INDEX IF NOT EXISTS dungeon_race_spot_times_idx ON dungeon_race_matches(dungeon_id, spot_id, spot_revision, boss_id, status)");
|
||||
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, boss_id, affix_ids_json)
|
||||
@@ -104,6 +113,7 @@ export function createDungeonRaceRepository(database, { ErrorType, isGameMaster
|
||||
const match = matchById.get(id);
|
||||
if (!match) return null;
|
||||
return { id: match.id, dungeonId: match.dungeon_id, seed: match.seed,
|
||||
...(match.selection_json ? { biteSizedSelection: JSON.parse(match.selection_json) } : {}),
|
||||
difficultyId: match.difficulty_id, bossDifficulty: match.boss_difficulty,
|
||||
status: match.status, createdAt: match.created_at, startAt: match.start_at,
|
||||
settledAt: match.settled_at, winnerSide: match.winner_side,
|
||||
@@ -114,12 +124,16 @@ export function createDungeonRaceRepository(database, { ErrorType, isGameMaster
|
||||
readyAt: row.ready_at, clearAt: row.clear_at, durationMs: row.duration_ms, outcome: row.outcome })) };
|
||||
}
|
||||
function createMatch(match) {
|
||||
const selection = match.biteSizedSelection ?? biteSizedLayouts?.resolveBiteSizedRun(match.dungeonId, match.seed);
|
||||
transaction(() => {
|
||||
insertMatch.run(match.id, match.dungeonId, match.seed, match.difficultyId, match.createdAt,
|
||||
match.bot ? JSON.stringify(match.bot) : null, raceBossId(match.dungeonId, match.difficultyId, match.seed),
|
||||
JSON.stringify(match.affixIds ?? []));
|
||||
for (const participant of match.participants) insertParticipant.run(match.id, participant.side,
|
||||
participant.accountId, participant.characterId, participant.username, participant.source, participant.role);
|
||||
const scope = selection ? { spotId: selection.spot.id, spotRevision: selection.spot.revision } : defaultBiteSizedBoardScope(match.dungeonId);
|
||||
database.prepare("UPDATE dungeon_race_matches SET spot_id = ?, spot_revision = ?, selection_json = ?, boss_id = COALESCE(?, boss_id) WHERE id = ?")
|
||||
.run(scope.spotId, scope.spotRevision, selection ? JSON.stringify(selection) : null, selection?.boss.objectiveId ?? null, match.id);
|
||||
});
|
||||
return getMatch(match.id);
|
||||
}
|
||||
@@ -140,7 +154,7 @@ export function createDungeonRaceRepository(database, { ErrorType, isGameMaster
|
||||
|| 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);
|
||||
const selectedBossId = match.biteSizedSelection?.boss.objectiveId ?? 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))
|
||||
|| (selectedBossId && (bossIds.length !== 1 || bossIds[0] !== selectedBossId)))
|
||||
@@ -216,7 +230,7 @@ export function createDungeonRaceRepository(database, { ErrorType, isGameMaster
|
||||
return { wins: Number(row.wins ?? 0), losses: Number(row.losses ?? 0) };
|
||||
}
|
||||
function leaderboard({ view = "wins", period = "30d", dungeonId = null, bossId = null,
|
||||
limit = 20, offset = 0 } = {}, timestamp = Date.now()) {
|
||||
spotId = null, spotRevision = 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)
|
||||
@@ -230,7 +244,17 @@ export function createDungeonRaceRepository(database, { ErrorType, isGameMaster
|
||||
const byDungeon = dungeonId ? "AND m.dungeon_id = ?" : "";
|
||||
const selectedBossId = dungeonId && bossIds?.length ? bossId ?? bossIds[0] : null;
|
||||
const byBoss = selectedBossId ? "AND m.boss_id = ?" : "";
|
||||
const args = [since, ...(dungeonId ? [dungeonId] : []), ...(selectedBossId ? [selectedBossId] : [])];
|
||||
const defaults = defaultBiteSizedBoardScope(dungeonId);
|
||||
const selectedSpotId = view === "times" ? spotId ?? defaults.spotId : null;
|
||||
const selectedSpotRevision = view === "times" ? spotRevision == null ? defaults.spotRevision : Number(spotRevision) : null;
|
||||
if (selectedSpotId && (!Number.isSafeInteger(selectedSpotRevision) || selectedSpotRevision < 0)) throw new ErrorType("Choose a valid spot revision.", 400, "invalid_race_leaderboard");
|
||||
if (selectedSpotId && biteSizedLayouts) {
|
||||
const spot = biteSizedLayouts.getBiteSizedSpotRevision(selectedSpotId, selectedSpotRevision);
|
||||
if (!dungeonId.startsWith("randomized-bite-sized-") && spot.dungeonId !== dungeonId) throw new ErrorType("Choose a spot in this dungeon.", 400, "invalid_race_leaderboard");
|
||||
}
|
||||
const bySpot = selectedSpotId ? "AND m.spot_id = ? AND m.spot_revision = ?" : "";
|
||||
const spotArgs = selectedSpotId ? [selectedSpotId, selectedSpotRevision] : [];
|
||||
const args = [since, ...(dungeonId ? [dungeonId] : []), ...(selectedBossId ? [selectedBossId] : []), ...spotArgs];
|
||||
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,
|
||||
@@ -242,7 +266,7 @@ export function createDungeonRaceRepository(database, { ErrorType, isGameMaster
|
||||
: `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} ${byBoss} AND p.duration_ms IS NOT NULL
|
||||
WHERE p.clear_at >= ? ${byDungeon} ${byBoss} ${bySpot} 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;
|
||||
@@ -250,7 +274,7 @@ export function createDungeonRaceRepository(database, { ErrorType, isGameMaster
|
||||
const bestAffixes = view === "times" ? database.prepare(`SELECT m.affix_ids_json AS affixIdsJson
|
||||
FROM dungeon_race_participants p JOIN dungeon_race_matches m ON m.id = p.match_id
|
||||
WHERE p.account_id = ? AND m.dungeon_id = ? AND m.boss_id = ?
|
||||
AND p.duration_ms = ? AND p.clear_at >= ?
|
||||
AND p.duration_ms = ? AND p.clear_at >= ? ${bySpot}
|
||||
ORDER BY p.clear_at ASC LIMIT 1`) : null;
|
||||
const ranked = all.map((row, index) => {
|
||||
const value = view === "wins" ? Number(row.wins) : Number(row.durationMs);
|
||||
@@ -259,7 +283,7 @@ export function createDungeonRaceRepository(database, { ErrorType, isGameMaster
|
||||
return { ...row, rank, ...(view === "wins"
|
||||
? { wins: Number(row.wins), losses: Number(row.losses) }
|
||||
: { durationMs: Number(row.durationMs), affixIds: JSON.parse(bestAffixes.get(
|
||||
row.accountId, dungeonId, selectedBossId, row.durationMs, since)?.affixIdsJson ?? "[]") }) };
|
||||
row.accountId, dungeonId, selectedBossId, row.durationMs, since, ...spotArgs)?.affixIdsJson ?? "[]") }) };
|
||||
});
|
||||
return { view, period, dungeonId, bossId: selectedBossId,
|
||||
total: ranked.length, entries: ranked.slice(offset, offset + limit) };
|
||||
|
||||
Reference in new issue
Block a user