292 lines
9.4 KiB
TypeScript
292 lines
9.4 KiB
TypeScript
import { onlineApiOrigin, requestOnlineJson } from "./onlineAccountClient";
|
|
|
|
export type BalanceLabPartySource = "wow" | "coa" | "rom" | "all";
|
|
export type BalanceLabBossCategory = "all" | "wow" | "rom";
|
|
export type BalanceLabSpecSelection = "random" | "all" | "specific";
|
|
export type BalanceLabJobStatus = "queued" | "running" | "complete" | "failed";
|
|
export type BalanceLabExecutionTarget = "server" | "local";
|
|
|
|
function balanceLabApiPath(target: BalanceLabExecutionTarget, suffix: string): string {
|
|
const root = target === "local" ? "/api/local/balance-lab" : "/api/gm/balance-lab";
|
|
return `${root}${suffix}`;
|
|
}
|
|
|
|
async function requestBalanceLabJson<T>(
|
|
pathname: string,
|
|
init: RequestInit,
|
|
token: string,
|
|
target: BalanceLabExecutionTarget,
|
|
): Promise<T> {
|
|
if (target === "server") return requestOnlineJson<T>(pathname, init, token);
|
|
const headers = new Headers(init.headers);
|
|
headers.set("Accept", "application/json");
|
|
if (init.body !== undefined) headers.set("Content-Type", "application/json");
|
|
const response = await fetch(pathname, { ...init, headers });
|
|
const body = await response.json().catch(() => ({})) as T & { error?: string };
|
|
if (!response.ok) throw new Error(body.error ?? `Local Balance Lab request failed (${response.status}).`);
|
|
return body;
|
|
}
|
|
|
|
export interface BalanceLabRuntimeStatus {
|
|
readonly available: boolean;
|
|
readonly executable: string;
|
|
readonly version: string | null;
|
|
readonly error: string | null;
|
|
readonly maxConcurrent: number;
|
|
readonly running: number;
|
|
readonly queued: number;
|
|
readonly maxQueued: number;
|
|
}
|
|
|
|
export interface BalanceLabJobSpec {
|
|
readonly source: BalanceLabPartySource;
|
|
readonly specSelection?: BalanceLabSpecSelection;
|
|
readonly selectedBuildIds?: readonly string[];
|
|
readonly level: number;
|
|
readonly partyConfigurations: number;
|
|
readonly simulationsPerParty: number;
|
|
readonly durationSeconds: number;
|
|
readonly partySeed: number;
|
|
readonly bossCategory: BalanceLabBossCategory;
|
|
readonly bossKeys: readonly string[];
|
|
readonly seatSweep?: boolean;
|
|
readonly includeBetaSpecs?: boolean;
|
|
}
|
|
|
|
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 npcClassSpecs?: number;
|
|
readonly npcClassAbilities?: 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 characterName?: string | null;
|
|
readonly talentBuildId?: string | null;
|
|
readonly talentBuildName?: string | null;
|
|
readonly talentBuildSource?: string | null;
|
|
readonly talentBuildSourcePointCount?: number | null;
|
|
readonly talentBuildSourcePointsRetained?: number | null;
|
|
readonly talentBuildFallbackPointCount?: number | null;
|
|
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 hpsReceived?: number;
|
|
readonly dtps: number;
|
|
readonly gcdUptimePercent?: number;
|
|
readonly totalDamage: number;
|
|
readonly totalDamageTaken: number;
|
|
readonly totalHealing: number;
|
|
readonly totalHealingReceived?: 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 rotationProfileId?: string | null;
|
|
readonly rotationPriority?: number | null;
|
|
readonly expectedUsage?: "profile" | "conditional" | "available";
|
|
readonly rotationDecisions?: number;
|
|
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" | "missing-profile-action" | "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 npcClassSpecs?: readonly BalanceLabClassSpecResult[];
|
|
readonly npcClassAbilities?: readonly BalanceLabClassAbilityResult[];
|
|
readonly partyConfigurations: readonly BalanceLabPartyResult[];
|
|
}
|
|
|
|
export async function listBalanceLabJobs(
|
|
token: string,
|
|
target: BalanceLabExecutionTarget = "server",
|
|
): Promise<readonly BalanceLabJob[]> {
|
|
return (await requestBalanceLabJson<{ jobs: BalanceLabJob[] }>(
|
|
balanceLabApiPath(target, "/jobs"),
|
|
{},
|
|
token, target,
|
|
)).jobs;
|
|
}
|
|
|
|
export async function getBalanceLabRuntime(
|
|
token: string,
|
|
target: BalanceLabExecutionTarget = "server",
|
|
): Promise<BalanceLabRuntimeStatus> {
|
|
return (await requestBalanceLabJson<{ runtime: BalanceLabRuntimeStatus }>(
|
|
balanceLabApiPath(target, "/runtime"),
|
|
{},
|
|
token, target,
|
|
)).runtime;
|
|
}
|
|
|
|
export async function startBalanceLabJob(
|
|
spec: BalanceLabJobSpec,
|
|
token: string,
|
|
target: BalanceLabExecutionTarget = "server",
|
|
): Promise<BalanceLabJob> {
|
|
return (await requestBalanceLabJson<{ job: BalanceLabJob }>(
|
|
balanceLabApiPath(target, "/jobs"),
|
|
{ method: "POST", body: JSON.stringify(spec) },
|
|
token, target,
|
|
)).job;
|
|
}
|
|
|
|
export async function startBalanceLabJobs(
|
|
specs: readonly BalanceLabJobSpec[],
|
|
token: string,
|
|
target: BalanceLabExecutionTarget = "server",
|
|
): Promise<readonly BalanceLabJob[]> {
|
|
return (await requestBalanceLabJson<{ jobs: BalanceLabJob[] }>(
|
|
balanceLabApiPath(target, "/jobs/batch"),
|
|
{ method: "POST", body: JSON.stringify({ specs }) },
|
|
token, target,
|
|
)).jobs;
|
|
}
|
|
|
|
export async function getBalanceLabJob(
|
|
id: string,
|
|
token: string,
|
|
target: BalanceLabExecutionTarget = "server",
|
|
): Promise<BalanceLabJob> {
|
|
return (await requestBalanceLabJson<{ job: BalanceLabJob }>(
|
|
balanceLabApiPath(target, `/jobs/${encodeURIComponent(id)}`),
|
|
{},
|
|
token, target,
|
|
)).job;
|
|
}
|
|
|
|
export async function getBalanceLabReport(
|
|
id: string,
|
|
token: string,
|
|
target: BalanceLabExecutionTarget = "server",
|
|
): Promise<BalanceLabSuiteReport> {
|
|
return requestBalanceLabJson<BalanceLabSuiteReport>(
|
|
balanceLabApiPath(target, `/jobs/${encodeURIComponent(id)}/artifacts/suite-report.json`),
|
|
{},
|
|
token, target,
|
|
);
|
|
}
|
|
|
|
export async function fetchBalanceLabArtifact(
|
|
id: string,
|
|
name: string,
|
|
token: string,
|
|
target: BalanceLabExecutionTarget = "server",
|
|
): Promise<Blob> {
|
|
const response = await fetch(
|
|
`${target === "local" ? "" : onlineApiOrigin()}${balanceLabApiPath(target, `/jobs/${encodeURIComponent(id)}/artifacts/${encodeURIComponent(name)}`)}`,
|
|
{ headers: token ? { Authorization: `Bearer ${token}` } : undefined },
|
|
);
|
|
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();
|
|
}
|