import { onlineApiOrigin, requestOnlineJson } from "./onlineAccountClient"; export type BalanceLabPartySource = "wow" | "coa" | "rom" | "all"; export type BalanceLabBossCategory = "all" | "wow" | "rom"; export type BalanceLabJobStatus = "queued" | "running" | "complete" | "failed"; export interface BalanceLabRuntimeStatus { readonly available: boolean; readonly executable: string; readonly version: string | null; readonly error: string | null; } export interface BalanceLabJobSpec { readonly source: BalanceLabPartySource; readonly level: number; readonly partyConfigurations: number; readonly simulationsPerParty: number; readonly durationSeconds: number; readonly partySeed: number; readonly bossCategory: BalanceLabBossCategory; readonly bossKeys: readonly string[]; } export interface BalanceLabJob { readonly id: string; readonly status: BalanceLabJobStatus; readonly createdAt: string; readonly startedAt: string | null; readonly completedAt: string | null; readonly requestedBy: string; readonly spec: BalanceLabJobSpec; readonly progress: { readonly party: number; readonly partiesCompleted: number; readonly partiesTotal: number; readonly simulationsCompleted: number; readonly simulationsTotal: number; }; readonly artifacts: readonly string[]; readonly error: string | null; readonly logTail: readonly string[]; readonly summary: BalanceLabReportSummary | null; } export interface BalanceLabReportSummary { readonly partyConfigurations: number; readonly bosses: number; readonly runs: number; readonly classSpecs: number; readonly classAbilities: number; readonly bossPressureMedianDtps: number; } export interface BalanceLabBossResult { readonly bossKey: string; readonly bossName: string; readonly category: string; readonly dungeonId: string; readonly dungeonTitle: string; readonly runs: number; readonly incomingDtps: number; readonly averageIncomingDamage: number; readonly partyDps: number; readonly bossDpsTaken: number; readonly defeatRate: number; readonly wipeRate: number; readonly averageDeaths: number; readonly averageTimeToDefeat: number | null; readonly bossMaxHealth: number; readonly pressureVsMedian: number | null; readonly pressureFlag: string; } export interface BalanceLabClassSpecResult { readonly source: string; readonly buildId: string; readonly classId: string; readonly className: string; readonly specialization: string; readonly role: string; readonly partyConfigurations: number; readonly runs: number; readonly dps: number; readonly bossDps: number; readonly hps: number; readonly dtps: number; readonly totalDamage: number; readonly totalDamageTaken: number; readonly totalHealing: number; readonly totalAbsorbs: number; readonly deathEvents: number; readonly damageVsSourceRoleMedian: number | null; readonly damageFlag: string; } export interface BalanceLabPartyResult { readonly partyConfigurationId: string; readonly source: string; readonly members: string; readonly runs: number; readonly partyDps: number; readonly incomingDtps: number; readonly defeatRate: number; readonly wipeRate: number; readonly averageDeaths: number; readonly reportPath: string; } export interface BalanceLabClassAbilityResult { readonly source: string; readonly buildId: string; readonly classId: string; readonly className: string; readonly specialization: string; readonly role: string; readonly abilityId: string; readonly abilityName: string; readonly icon: string; readonly categories: string; readonly cooldownSeconds: number | null; readonly partyConfigurations: number; readonly bosses: number; readonly runs: number; readonly availableRuns: number; readonly activeRuns: number; readonly usageRate: number; readonly casts: number; readonly castsPerMinute: number; readonly hits: number; readonly hitsPerRun: number; readonly rawDamage: number; readonly effectiveDamage: number; readonly dps: number; readonly damageShare: number; readonly averageDamagePerHit: number; readonly rawHealing: number; readonly effectiveHealing: number; readonly overheal: number; readonly absorbs: number; readonly status: "unused" | "damage" | "support" | "used"; readonly unusedAvailable: boolean; } export interface BalanceLabSuiteReport { readonly schemaVersion: number; readonly generatedAt: string; readonly engine: string; readonly level: number; readonly summary: BalanceLabReportSummary; readonly bosses: readonly BalanceLabBossResult[]; readonly classSpecs: readonly BalanceLabClassSpecResult[]; readonly classAbilities?: readonly BalanceLabClassAbilityResult[]; readonly partyConfigurations: readonly BalanceLabPartyResult[]; } export async function listBalanceLabJobs(token: string): Promise { return (await requestOnlineJson<{ jobs: BalanceLabJob[] }>( "/api/gm/balance-lab/jobs", {}, token, )).jobs; } export async function getBalanceLabRuntime(token: string): Promise { return (await requestOnlineJson<{ runtime: BalanceLabRuntimeStatus }>( "/api/gm/balance-lab/runtime", {}, token, )).runtime; } export async function startBalanceLabJob(spec: BalanceLabJobSpec, token: string): Promise { return (await requestOnlineJson<{ job: BalanceLabJob }>( "/api/gm/balance-lab/jobs", { method: "POST", body: JSON.stringify(spec) }, token, )).job; } export async function getBalanceLabJob(id: string, token: string): Promise { return (await requestOnlineJson<{ job: BalanceLabJob }>( `/api/gm/balance-lab/jobs/${encodeURIComponent(id)}`, {}, token, )).job; } export async function getBalanceLabReport(id: string, token: string): Promise { return requestOnlineJson( `/api/gm/balance-lab/jobs/${encodeURIComponent(id)}/artifacts/suite-report.json`, {}, token, ); } export async function fetchBalanceLabArtifact(id: string, name: string, token: string): Promise { const response = await fetch( `${onlineApiOrigin()}/api/gm/balance-lab/jobs/${encodeURIComponent(id)}/artifacts/${encodeURIComponent(name)}`, { headers: { Authorization: `Bearer ${token}` } }, ); if (!response.ok) { const body = await response.json().catch(() => ({})) as { error?: string }; throw new Error(body.error ?? `Could not download ${name} (${response.status}).`); } return response.blob(); }