diff options
Diffstat (limited to 'webAO/viewport/utils/preloadMessageAssets.ts')
| -rw-r--r-- | webAO/viewport/utils/preloadMessageAssets.ts | 151 |
1 files changed, 151 insertions, 0 deletions
diff --git a/webAO/viewport/utils/preloadMessageAssets.ts b/webAO/viewport/utils/preloadMessageAssets.ts new file mode 100644 index 0000000..0a24e84 --- /dev/null +++ b/webAO/viewport/utils/preloadMessageAssets.ts @@ -0,0 +1,151 @@ +import { ChatMsg } from "../interfaces/ChatMsg"; +import { PreloadedAssets } from "../interfaces/PreloadedAssets"; +import { + resolveAndPreloadImage, + resolveAndPreloadAudio, + getAnimDuration, +} from "../../utils/assetCache"; +import transparentPng from "../../constants/transparentPng"; + +const GLOBAL_TIMEOUT_MS = 8000; + +/** + * Builds the list of candidate URLs for a character emote across all extensions. + * Replicates the URL construction logic from setEmote.ts. + */ +function buildEmoteUrls( + AO_HOST: string, + extensions: string[], + charactername: string, + emotename: string, + prefix: string, +): string[] { + const characterFolder = `${AO_HOST}characters/`; + const urls: string[] = []; + + for (const extension of extensions) { + let url: string; + if (extension === ".png") { + url = `${characterFolder}${encodeURI(charactername)}/${encodeURI(emotename)}${extension}`; + } else if (extension === ".webp.static") { + url = `${characterFolder}${encodeURI(charactername)}/${encodeURI(emotename)}.webp`; + } else { + url = `${characterFolder}${encodeURI(charactername)}/${encodeURI(prefix)}${encodeURI(emotename)}${extension}`; + } + urls.push(url); + } + + return urls; +} + +const DEFAULT_ASSETS: PreloadedAssets = { + idleUrl: transparentPng, + talkingUrl: transparentPng, + preanimUrl: transparentPng, + preanimDuration: 0, + pairIdleUrl: transparentPng, + shoutSfxUrl: null, + emoteSfxUrl: null, + realizationSfxUrl: null, + stabSfxUrl: null, +}; + +/** + * Preloads all assets referenced in an IC message before the animation timeline starts. + * Resolves all file extensions in parallel and primes the browser cache. + * All resolution and preloading is cached via assetCache, so repeated messages + * from the same character with the same emotes resolve near-instantly. + */ +export default async function preloadMessageAssets( + chatmsg: ChatMsg, + AO_HOST: string, + emoteExtensions: string[], +): Promise<PreloadedAssets> { + const charName = chatmsg.name!.toLowerCase(); + const charEmote = chatmsg.sprite!.toLowerCase(); + + const doPreload = async (): Promise<PreloadedAssets> => { + // Build candidate URL lists for each emote + const idleUrls = buildEmoteUrls(AO_HOST, emoteExtensions, charName, charEmote, "(a)"); + const talkingUrls = buildEmoteUrls(AO_HOST, emoteExtensions, charName, charEmote, "(b)"); + + const hasPreanim = + chatmsg.type === 1 && + chatmsg.preanim && + chatmsg.preanim !== "-" && + chatmsg.preanim !== ""; + + const preanimUrls = hasPreanim + ? buildEmoteUrls(AO_HOST, emoteExtensions, charName, chatmsg.preanim!.toLowerCase(), "") + : null; + + const hasPair = !!chatmsg.other_name; + const pairIdleUrls = hasPair + ? buildEmoteUrls(AO_HOST, emoteExtensions, chatmsg.other_name!.toLowerCase(), chatmsg.other_emote!.toLowerCase(), "(a)") + : null; + + // Shout SFX per-character path + const shoutNames = [undefined, "holdit", "objection", "takethat", "custom"]; + const shoutName = shoutNames[chatmsg.objection]; + const shoutSfxPath = (chatmsg.objection > 0 && chatmsg.objection < 4 && shoutName) + ? `${AO_HOST}characters/${encodeURI(charName)}/${shoutName}.opus` + : null; + + // Emote SFX + const invalidSounds = ["0", "1", "", undefined]; + const emoteSfxPath = ( + !invalidSounds.includes(chatmsg.sound) && + (chatmsg.type == 1 || chatmsg.type == 2 || chatmsg.type == 6) + ) ? `${AO_HOST}sounds/general/${encodeURI(chatmsg.sound.toLowerCase())}.opus` + : null; + + // Realization and stab SFX (always preloaded - used by \f and \s text commands) + const realizationPath = `${AO_HOST}sounds/general/sfx-realization.opus`; + const stabPath = `${AO_HOST}sounds/general/sfx-stab.opus`; + + // Launch everything in parallel - assetCache handles per-URL dedup and caching + const [ + idleUrl, talkingUrl, preanimUrl, preanimDuration, pairIdleUrl, + shoutSfxUrl, emoteSfxUrl, realizationSfxUrl, stabSfxUrl, + ] = await Promise.all([ + resolveAndPreloadImage(idleUrls), + resolveAndPreloadImage(talkingUrls), + preanimUrls ? resolveAndPreloadImage(preanimUrls) : Promise.resolve(transparentPng), + hasPreanim + ? getAnimDuration(`${AO_HOST}characters/${encodeURI(charName)}/${encodeURI(chatmsg.preanim!.toLowerCase())}`) + : Promise.resolve(0), + pairIdleUrls ? resolveAndPreloadImage(pairIdleUrls) : Promise.resolve(transparentPng), + shoutSfxPath ? resolveAndPreloadAudio(shoutSfxPath) : Promise.resolve(null), + emoteSfxPath ? resolveAndPreloadAudio(emoteSfxPath) : Promise.resolve(null), + resolveAndPreloadAudio(realizationPath), + resolveAndPreloadAudio(stabPath), + ]); + + return { + idleUrl, + talkingUrl, + preanimUrl, + preanimDuration, + pairIdleUrl, + shoutSfxUrl, + emoteSfxUrl, + realizationSfxUrl, + stabSfxUrl, + }; + }; + + // Race against global timeout for graceful degradation + const timeoutPromise = new Promise<PreloadedAssets>((resolve) => { + setTimeout(() => { + console.warn("Asset preloading timed out, using defaults"); + resolve({ ...DEFAULT_ASSETS }); + }, GLOBAL_TIMEOUT_MS); + }); + + try { + return await Promise.race([doPreload(), timeoutPromise]); + } catch (error) { + console.error("Asset preloading failed:", error); + return { ...DEFAULT_ASSETS }; + } +} |
