Update Healer Man server

This commit is contained in:
phenom committed 2026-09-27 14:31:15 -04:00
1 parent ab1434a3b8
commit e284f4fc76
60 files changed
+2704 -118

No files matched your search

+42 -7
View File
@@ -8,6 +8,10 @@ import { dungeonContentIsUsable, installContentPack } from "../content/contentMa
import { ensureRandomizedBiteSizedRunContent } from "./randomizedBiteSizedContent";
import { createRaceBot, raceBotProgress, type RaceBot, type RaceBotSource, type RaceBotTier } from "./dungeonRaceBot";
import type { PartyRole } from "../game/partyRoles";
import { finishRaceCombatLog, startRaceCombatLog, stopRaceCombatLog,
type RaceCombatLog } from "../game/dungeonRaceCombatLog";
import { rollDungeonRaceAffixes, validDungeonRaceAffixes, type DungeonRaceAffixPair } from "../game/dungeonRaceAffixes";
import { cleanupDungeonRaceAffixes } from "../game/dungeonRaceAffixLifecycle";
export type RacePool = "authored" | "randomized";
export type RaceStatus = "idle" | "queued" | "preparing" | "countdown" | "active" | "settled" | "void";
@@ -26,6 +30,7 @@ export interface RaceServerState {
readonly seed?: string;
readonly dungeonId?: string | null;
readonly difficultyId?: string;
readonly affixIds?: readonly string[];
readonly startAt?: number | null;
readonly prepareDeadline?: number;
readonly ready?: boolean;
@@ -42,6 +47,7 @@ interface RaceState {
seed: string | null;
dungeonId: string | null;
difficultyId: string | null;
affixIds: DungeonRaceAffixPair | null;
startAt: number | null;
serverOffset: number;
opponent: RaceOpponent | null;
@@ -77,6 +83,7 @@ let queueRequest: Promise<RaceServerState> | null = null;
let pendingLeave: Promise<void> | null = null;
let pendingCleanupToken: string | null = null;
let raceRevision = 0;
let pendingCombatLog: RaceCombatLog | null = null;
function stopPolling() { if (pollTimer) clearInterval(pollTimer); pollTimer = null; }
function errorMessage(error: unknown): string { return error instanceof Error ? error.message : "The race could not continue."; }
@@ -99,6 +106,9 @@ export const useDungeonRaceStore = create<RaceState>((set, get) => {
try {
await prepareContent(dungeonId, seed);
if (raceRevision !== revision || get().seed !== seed || get().status === "void") return;
if (!validDungeonRaceAffixes(get().affixIds)) {
throw new Error("This race uses mechanics your client does not support. Update the game to race.");
}
const { installDungeonShellRuntime } = await import("./dungeonShellRuntime");
if (raceRevision !== revision || get().seed !== seed) return;
installDungeonShellRuntime();
@@ -106,8 +116,10 @@ export const useDungeonRaceStore = create<RaceState>((set, get) => {
freshRun: true, runId: seed, difficultyId, bossDifficulty: 1, raceStartBlocked: true,
});
if (!entered) throw new Error(useShellStore.getState().notice || "Could not enter the race dungeon.");
startRaceCombatLog(seed, dungeonId);
set({ inGame: true, preparingContent: false });
} catch (error) {
stopRaceCombatLog();
if (raceRevision === revision) {
set({ error: errorMessage(error), preparingContent: false });
if (get().mode === "online") await get().leave();
@@ -125,8 +137,16 @@ export const useDungeonRaceStore = create<RaceState>((set, get) => {
&& RACE_STATUS_ORDER[response.status] < RACE_STATUS_ORDER[previous.status]) return;
const result = response.result ?? null;
const offset = response.serverTime - Date.now();
if (response.matchId && !validDungeonRaceAffixes(response.affixIds)) {
const message = "This race uses mechanics your client does not support. Update the game to race.";
void get().leave().finally(() => {
if (get().mode === null) set({ error: message });
});
return;
}
set({ status: response.status, matchId: response.matchId ?? null, seed: response.seed ?? null,
dungeonId: response.dungeonId ?? null, difficultyId: response.difficultyId ?? null,
affixIds: validDungeonRaceAffixes(response.affixIds) ? response.affixIds : null,
startAt: response.startAt ?? null,
serverOffset: offset, opponent: response.opponent ?? null,
opponentProgress: response.opponentProgress ?? null, result,
@@ -138,7 +158,7 @@ export const useDungeonRaceStore = create<RaceState>((set, get) => {
if (["settled", "void", "idle"].includes(response.status)) stopPolling();
}
return {
mode: null, status: "idle", matchId: null, seed: null, dungeonId: null, difficultyId: null, startAt: null,
mode: null, status: "idle", matchId: null, seed: null, dungeonId: null, difficultyId: null, affixIds: null, startAt: null,
serverOffset: 0, opponent: null, opponentProgress: null, result: null, clearDurationMs: null,
ready: false, inGame: false, preparingContent: false, toastAt: null, error: "",
queueAt: null, botAt: null,
@@ -187,9 +207,10 @@ export const useDungeonRaceStore = create<RaceState>((set, get) => {
set({ error: "No Bite-Sized dungeon is available for practice." }); return;
}
const seed = `practice-${globalThis.crypto.randomUUID()}`;
const affixIds = rollDungeonRaceAffixes(seed);
set({ mode: "practice", status: "preparing", matchId: seed, seed, dungeonId: resolvedDungeon,
difficultyId: null,
opponent: createRaceBot(seed, tier, source) });
difficultyId: null, affixIds,
opponent: createRaceBot(seed, tier, source, affixIds) });
await enterPreparedRun(resolvedDungeon, seed);
},
sync: async () => {
@@ -223,6 +244,8 @@ export const useDungeonRaceStore = create<RaceState>((set, get) => {
if (finishPromise) return finishPromise;
if (!state.matchId || !state.startAt || ["void", "idle"].includes(state.status)
|| state.clearDurationMs != null) return Promise.resolve();
pendingCombatLog ??= finishRaceCombatLog(state.seed ?? state.matchId,
state.startAt - state.serverOffset, state.mode === "practice");
if (state.mode === "practice") {
const now = Date.now();
const result = state.result ?? (now < state.startAt + (state.opponent as RaceBot).finishMs ? "win" : "loss");
@@ -234,9 +257,9 @@ export const useDungeonRaceStore = create<RaceState>((set, get) => {
const matchId = state.matchId;
const report = requestOnlineJson<RaceServerState>(
`/api/dungeon-races/${encodeURIComponent(state.matchId)}/finish`, {
method: "POST", body: JSON.stringify({ runId: state.seed, bossIds }),
method: "POST", body: JSON.stringify({ runId: state.seed, bossIds, combatLog: pendingCombatLog }),
}, sessionToken)
.then((response) => { if (get().matchId === matchId) applyServerState(response); })
.then((response) => { if (get().matchId === matchId) { pendingCombatLog = null; applyServerState(response); } })
.catch((error: unknown) => { if (get().matchId === matchId) set({ error: errorMessage(error) }); })
.finally(() => { if (finishPromise === report) finishPromise = null; });
finishPromise = report;
@@ -252,7 +275,11 @@ export const useDungeonRaceStore = create<RaceState>((set, get) => {
opponentProgress: raceBotProgress(now - state.startAt, state.opponent) });
},
leave: async () => {
const token = get().mode === "online" ? sessionToken : null;
const departing = get();
const token = departing.mode === "online" ? sessionToken : null;
const partialLog = departing.seed && departing.startAt && departing.clearDurationMs === null
? finishRaceCombatLog(departing.seed, departing.startAt - departing.serverOffset,
departing.mode === "practice", false) : null;
const request = queueRequest;
get().reset();
const revision = raceRevision;
@@ -260,6 +287,12 @@ export const useDungeonRaceStore = create<RaceState>((set, get) => {
pendingCleanupToken = token;
const closing = (async () => {
try { await request; } catch { /* A failed queue request needs no server cleanup. */ }
if (partialLog && departing.matchId) {
try {
await requestOnlineJson(`/api/dungeon-races/${encodeURIComponent(departing.matchId)}/combat-log`,
{ method: "POST", body: JSON.stringify({ combatLog: partialLog }) }, token);
} catch (error) { if (raceRevision === revision) set({ error: errorMessage(error) }); }
}
await requestOnlineJson("/api/dungeon-races/queue", { method: "DELETE" }, token);
if (pendingCleanupToken === token) pendingCleanupToken = null;
})();
@@ -270,8 +303,10 @@ export const useDungeonRaceStore = create<RaceState>((set, get) => {
},
reset: () => {
raceRevision += 1;
cleanupDungeonRaceAffixes();
stopPolling(); sessionToken = null; preparingSeed = null; readyPending = false; finishPromise = null;
set({ mode: null, status: "idle", matchId: null, seed: null, dungeonId: null, difficultyId: null, startAt: null,
stopRaceCombatLog(); pendingCombatLog = null;
set({ mode: null, status: "idle", matchId: null, seed: null, dungeonId: null, difficultyId: null, affixIds: null, startAt: null,
serverOffset: 0, opponent: null, opponentProgress: null, result: null, clearDurationMs: null,
ready: false, inGame: false, preparingContent: false, toastAt: null, error: "", queueAt: null, botAt: null });
},