Release Healer Man 0.1.16

This commit is contained in:
phenom committed 2026-09-05 20:42:38 -04:00
1 parent f49e54f35f
commit b57d2b2839
89 files changed
+6055 -17

No files matched your search

+76
View File
@@ -0,0 +1,76 @@
import type { PlayerSession } from "./types";
import { onlineApiOrigin, requestOnlineJson } from "./onlineAccountClient";
import type { ArenaLoadout, ArenaQueueRequest, ArenaQueueState, ArenaStageManifest } from "../game/arenaTypes";
export interface ArenaStateResponse {
characterId: string | null;
loadouts: ArenaLoadout[];
ratings: Record<"2v2" | "3v3", { bracket: "2v2" | "3v3"; rating: number; title: string; updatedAt: number | null } | null>;
queue: ArenaQueueState;
stages: ArenaStageManifest[];
}
function token(session: PlayerSession): string {
if (session.kind !== "account" || !session.accessToken) throw new Error("Rated arenas require an online account.");
return session.accessToken;
}
export function fetchArenaState(session: PlayerSession, characterId: string): Promise<ArenaStateResponse> {
return requestOnlineJson(`/api/arena/state?characterId=${encodeURIComponent(characterId)}`, {}, token(session));
}
export function putArenaLoadout(
session: PlayerSession,
characterId: string,
slot: number,
loadout: Omit<ArenaLoadout, "slot" | "characterId" | "level" | "version" | "selected" | "updatedAt"> & { selected?: boolean },
): Promise<ArenaLoadout> {
return requestOnlineJson(`/api/arena/loadouts/${slot}`, {
method: "PUT",
body: JSON.stringify({ ...loadout, characterId }),
}, token(session));
}
export function deleteArenaLoadout(session: PlayerSession, characterId: string, slot: number) {
return requestOnlineJson<{ ok: true }>(`/api/arena/loadouts/${slot}`, {
method: "DELETE",
body: JSON.stringify({ characterId }),
}, token(session));
}
export function enterArenaQueue(session: PlayerSession, request: ArenaQueueRequest): Promise<ArenaQueueState> {
return requestOnlineJson("/api/arena/queue", { method: "POST", body: JSON.stringify(request) }, token(session));
}
export function leaveArenaQueue(session: PlayerSession): Promise<ArenaQueueState> {
return requestOnlineJson("/api/arena/queue", { method: "DELETE" }, token(session));
}
export function acceptArenaReadyCheck(session: PlayerSession): Promise<ArenaQueueState> {
return requestOnlineJson("/api/arena/ready", { method: "POST" }, token(session));
}
interface ArenaSocketTicket { ticket: string; expiresAt: number; path: string }
export async function openArenaSocket(
session: PlayerSession,
onMessage: (message: unknown) => void,
onClose: () => void,
): Promise<WebSocket> {
const response = await requestOnlineJson<ArenaSocketTicket>("/api/arena/socket-ticket", { method: "POST" }, token(session));
const origin = onlineApiOrigin();
const base = origin || window.location.origin;
const url = new URL(response.path, base);
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
url.searchParams.set("ticket", response.ticket);
const socket = new WebSocket(url);
socket.addEventListener("message", (event) => {
try { onMessage(JSON.parse(String(event.data))); } catch { /* Ignore malformed server messages. */ }
});
socket.addEventListener("close", onClose);
await new Promise<void>((resolve, reject) => {
socket.addEventListener("open", () => resolve(), { once: true });
socket.addEventListener("error", () => reject(new Error("Could not connect to the arena server.")), { once: true });
});
return socket;
}
+2 -2
View File
@@ -40,7 +40,7 @@ let pendingCharacters: CharacterProfile[] | null = null;
let saveTimer: ReturnType<typeof setTimeout> | null = null;
let saveQueue = Promise.resolve();
function apiOrigin(): string {
export function onlineApiOrigin(): string {
const configured = String(import.meta.env.VITE_API_BASE_URL ?? "").replace(/\/+$/, "");
if (configured) return configured;
return Capacitor.isNativePlatform() ? NATIVE_API_ORIGIN : "";
@@ -53,7 +53,7 @@ export async function requestOnlineJson<T>(pathname: string, init: RequestInit =
if (token) headers.set("Authorization", `Bearer ${token}`);
let response: Response;
try {
response = await fetch(`${apiOrigin()}${pathname}`, { ...init, headers });
response = await fetch(`${onlineApiOrigin()}${pathname}`, { ...init, headers });
} catch {
throw new OnlineApiError("The online server is unavailable. Check your connection or play offline.", 0);
}
+10
View File
@@ -86,6 +86,7 @@ export interface ShellState {
openMainMenu: () => boolean;
openDungeons: () => boolean;
openManastorms: () => boolean;
openArenas: () => boolean;
openGmAdmin: () => boolean;
openClassHall: () => boolean;
changeSelectedSpecialization: (specializationId: string, resetTalents: boolean) => boolean;
@@ -333,6 +334,15 @@ export const useShellStore = create<ShellState>((set, get) => ({
set({ phase: "manastorms", activeCharacter: null, notice: "" });
return true;
},
openArenas: () => {
const { session, selectedCharacterId, characters } = get();
if (!session || !selectedCharacterId || !characters.some((character) => character.id === selectedCharacterId)) {
set({ phase: session ? "characters" : "login", notice: "Select or create a character first." });
return false;
}
set({ phase: "arenas", activeCharacter: null, notice: "" });
return true;
},
openGmAdmin: () => {
const { session, selectedCharacterId, characters } = get();
if (!isGameMasterSession(session)) {
+1 -1
View File
@@ -5,7 +5,7 @@ import type { EquipmentAssignments } from "../game/equipment";
import type { GameplaySettings } from "../game/combatStore";
import type { ManastormProgress } from "../game/manastormProgress";
export type ShellPhase = "login" | "characters" | "create-character" | "main-menu" | "class-hall" | "dungeons" | "manastorms" | "gm-admin" | "game";
export type ShellPhase = "login" | "characters" | "create-character" | "main-menu" | "class-hall" | "dungeons" | "manastorms" | "arenas" | "arena-game" | "gm-admin" | "game";
export type SessionKind = "account" | "offline";
export type AccountRole = "gm";