Files
healer-man/src/game/combatMediaPack.ts
T
2026-09-11 18:49:33 -04:00

87 lines
2.9 KiB
TypeScript

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<void> | 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<Response> {
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<Response> {
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<CombatMediaDownloadResult> {
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<boolean> {
if (combatMediaPackReady()) return true;
try {
await downloadCombatMediaPack();
return combatMediaPackReady();
} catch {
return false;
}
}
export async function removeCombatMediaPack(): Promise<void> {
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);
}