import { contentPackIsUsable, getContentManagerSnapshot, installContentPack, removeContentPack, resolveContentUrl, subscribeContentManager, } from "../content/contentManager"; export const COMBAT_MEDIA_MANIFEST_URL = "/assets/game/combat-effects/manifest.json"; export const COMBAT_MEDIA_CONTENT_PACK_ID = "core:combat-media"; export interface CombatMediaDownloadProgress { readonly completed: number; readonly total: number; } export interface CombatMediaDownloadResult { readonly fileCount: number; } type ProgressListener = (progress: CombatMediaDownloadProgress) => void; let installRequest: Promise | null = null; function isCombatMediaUrl(value: unknown): value is string { return typeof value === "string" && value.startsWith("/assets/game/combat-effects/"); } /** Fetch original media after the enabled pack is ready. */ export async function fetchCombatMediaAsset(url: string): Promise { if (!isCombatMediaUrl(url)) throw new Error(`Unsupported combat media URL: ${url}`); return fetchOptionalMediaAsset(url); } /** Fetch source media only after its shared optional pack has been enabled. */ export async function fetchOptionalMediaAsset(url: string): Promise { if (!combatMediaPackReady()) throw new Error("Original spell media is not installed."); return fetch(resolveContentUrl(url), { cache: "no-store" }); } /** Download and verify every file owned by the original combat-media pack. */ export async function downloadCombatMediaPack( onProgress?: ProgressListener, ): Promise { const publishProgress = () => { const state = getContentManagerSnapshot(); onProgress?.({ completed: state.downloadedBytes, total: Math.max(1, state.totalBytes), }); }; const unsubscribe = subscribeContentManager(publishProgress); try { installRequest ??= installContentPack(COMBAT_MEDIA_CONTENT_PACK_ID) .finally(() => { installRequest = null; }); await installRequest; publishProgress(); const pack = getContentManagerSnapshot().manifest?.packs.find((candidate) => ( candidate.id === COMBAT_MEDIA_CONTENT_PACK_ID )); return { fileCount: pack?.fileCount ?? 0 }; } finally { unsubscribe(); } } /** Prepare the default media without blocking gameplay when it is unavailable. */ export async function ensureCombatMediaPack(): Promise { if (combatMediaPackReady()) return true; try { await downloadCombatMediaPack(); return combatMediaPackReady(); } catch { return false; } } export async function removeCombatMediaPack(): Promise { await removeContentPack(COMBAT_MEDIA_CONTENT_PACK_ID); } export function combatMediaPackReady(): boolean { // Vite serves the complete source media tree, just as it does dungeon assets. // Shipped builds still require the verified optional content pack. return import.meta.env.DEV || contentPackIsUsable(COMBAT_MEDIA_CONTENT_PACK_ID); }