aboutsummaryrefslogtreecommitdiff
path: root/webAO/viewport/utils
diff options
context:
space:
mode:
Diffstat (limited to 'webAO/viewport/utils')
-rw-r--r--webAO/viewport/utils/handleICSpeaking.ts57
-rw-r--r--webAO/viewport/utils/preloadMessageAssets.ts151
2 files changed, 171 insertions, 37 deletions
diff --git a/webAO/viewport/utils/handleICSpeaking.ts b/webAO/viewport/utils/handleICSpeaking.ts
index ec791c5..a612e77 100644
--- a/webAO/viewport/utils/handleICSpeaking.ts
+++ b/webAO/viewport/utils/handleICSpeaking.ts
@@ -2,17 +2,16 @@ import { ChatMsg } from "../interfaces/ChatMsg";
import { client } from "../../client";
import { appendICLog } from "../../client/appendICLog";
import { checkCallword } from "../../client/checkCallword";
-import setEmote from "../../client/setEmote";
+import setEmoteFromUrl from "../../client/setEmoteFromUrl";
import { AO_HOST } from "../../client/aoHost";
import { SHOUTS } from "../constants/shouts";
-import getAnimLength from "../../utils/getAnimLength";
import { setChatbox } from "../../dom/setChatbox";
import { resizeChatbox } from "../../dom/resizeChatbox";
import transparentPng from "../../constants/transparentPng";
import { COLORS } from "../constants/colors";
import mlConfig from "../../utils/aoml";
import request from "../../services/request";
-import fileExists from "../../utils/fileExists";
+import preloadMessageAssets from "./preloadMessageAssets";
let attorneyMarkdown: ReturnType<typeof mlConfig> | null = null;
@@ -41,7 +40,7 @@ export const setStartThirdTickCheck = (val: boolean) => {
* This sets up everything before the tick() loops starts
* a lot of things can probably be moved here, like starting the shout animation if there is one
* TODO: the preanim logic, on the other hand, should probably be moved to tick()
- * @param {object} chatmsg the new chat message
+ * @param playerChatMsg the new chat message
*/
export const handle_ic_speaking = async (playerChatMsg: ChatMsg) => {
client.viewport.setChatmsg(playerChatMsg);
@@ -125,26 +124,19 @@ export const handle_ic_speaking = async (playerChatMsg: ChatMsg) => {
client.viewport.getSfxAudio(),
);
- setEmote(
+ // Preload all assets before any visual changes - resolves URLs and primes browser cache
+ const preloaded = await preloadMessageAssets(
+ client.viewport.getChatmsg(),
AO_HOST,
- client,
- client.viewport.getChatmsg().name!.toLowerCase(),
- client.viewport.getChatmsg().sprite!,
- "(a)",
- false,
- client.viewport.getChatmsg().side,
+ client.emote_extensions,
);
+ client.viewport.getChatmsg().preloadedAssets = preloaded;
+
+ // Set initial idle emote using pre-cached URLs (synchronous, images already in cache)
+ setEmoteFromUrl(preloaded.idleUrl, false, client.viewport.getChatmsg().side);
if (client.viewport.getChatmsg().other_name) {
- setEmote(
- AO_HOST,
- client,
- client.viewport.getChatmsg().other_name.toLowerCase(),
- client.viewport.getChatmsg().other_emote!,
- "(a)",
- false,
- client.viewport.getChatmsg().side,
- );
+ setEmoteFromUrl(preloaded.pairIdleUrl, true, client.viewport.getChatmsg().side);
}
// gets which shout shall played
@@ -164,11 +156,8 @@ export const handle_ic_speaking = async (playerChatMsg: ChatMsg) => {
}
shoutSprite.style.display = "block";
- const perCharPath = `${AO_HOST}characters/${encodeURI(
- client.viewport.getChatmsg().name.toLowerCase(),
- )}/${shout}.opus`;
- const exists = await fileExists(perCharPath);
- client.viewport.shoutaudio.src = exists ? perCharPath : client.resources[shout].sfx;
+ // Use preloaded shout SFX URL (already resolved in parallel)
+ client.viewport.shoutaudio.src = preloaded.shoutSfxUrl ?? client.resources[shout].sfx;
client.viewport.shoutaudio.play().catch(() => {});
client.viewport.setShoutTimer(client.resources[shout].duration);
} else {
@@ -176,28 +165,22 @@ export const handle_ic_speaking = async (playerChatMsg: ChatMsg) => {
}
client.viewport.getChatmsg().startpreanim = true;
- let gifLength = 0;
- if (
+ // Use preloaded preanim duration (already computed in parallel by preloader)
+ const hasPreanim =
client.viewport.getChatmsg().type === 1 &&
client.viewport.getChatmsg().preanim !== "-" &&
- client.viewport.getChatmsg().preanim !== ""
- ) {
- //we have a preanim
- chatContainerBox.style.opacity = "0";
+ client.viewport.getChatmsg().preanim !== "";
- gifLength = await getAnimLength(
- `${AO_HOST}characters/${encodeURI(
- client.viewport.getChatmsg().name!.toLowerCase(),
- )}/${encodeURI(client.viewport.getChatmsg().preanim)}`,
- );
+ if (hasPreanim) {
+ chatContainerBox.style.opacity = "0";
client.viewport.getChatmsg().startspeaking = false;
} else {
client.viewport.getChatmsg().startspeaking = true;
if (client.viewport.getChatmsg().content.trim() !== "")
chatContainerBox.style.opacity = "1";
}
- client.viewport.getChatmsg().preanimdelay = gifLength;
+ client.viewport.getChatmsg().preanimdelay = preloaded.preanimDuration;
const setAside = {
position: client.viewport.getChatmsg().side,
showSpeedLines: false,
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 };
+ }
+}