aboutsummaryrefslogtreecommitdiff
path: root/webAO
diff options
context:
space:
mode:
authorstonedDiscord <Tukz@gmx.de>2026-04-06 20:38:41 +0200
committerGitHub <noreply@github.com>2026-04-06 20:38:41 +0200
commit4be4f4665fe03a0267ac88c36f0e3b73d8fc2d48 (patch)
treeb76ef7b627523a8daebe0beb59e404d3da82d04e /webAO
parent815f56add06b92a48b964cb1343f70c86ea36435 (diff)
parent20810aa0d3dfac49e1f43fe84634f74f56374fcd (diff)
Merge pull request #301 from AttorneyOnline/rendering-fix
Fix IC rendering race conditions with asset preloading
Diffstat (limited to 'webAO')
-rw-r--r--webAO/client/setEmoteFromUrl.ts21
-rw-r--r--webAO/utils/assetCache.ts129
-rw-r--r--webAO/utils/fileExists.ts18
-rw-r--r--webAO/viewport/interfaces/ChatMsg.ts3
-rw-r--r--webAO/viewport/interfaces/PreloadedAssets.ts20
-rw-r--r--webAO/viewport/utils/handleICSpeaking.ts57
-rw-r--r--webAO/viewport/utils/preloadMessageAssets.ts151
-rw-r--r--webAO/viewport/viewport.ts92
8 files changed, 398 insertions, 93 deletions
diff --git a/webAO/client/setEmoteFromUrl.ts b/webAO/client/setEmoteFromUrl.ts
new file mode 100644
index 0000000..8075bbd
--- /dev/null
+++ b/webAO/client/setEmoteFromUrl.ts
@@ -0,0 +1,21 @@
+import transparentPng from "../constants/transparentPng";
+
+/**
+ * Sets a pre-resolved emote URL on the correct DOM <img> element.
+ * This is synchronous because the image should already be in the browser cache
+ * from preloading.
+ */
+const setEmoteFromUrl = (url: string, pair: boolean, side: string): void => {
+ const pairID = pair ? "pair" : "char";
+ const acceptedPositions = ["def", "pro", "wit"];
+ const position = acceptedPositions.includes(side) ? `${side}_` : "";
+ const emoteSelector = document.getElementById(
+ `client_${position}${pairID}_img`,
+ ) as HTMLImageElement;
+
+ if (emoteSelector) {
+ emoteSelector.src = url || transparentPng;
+ }
+};
+
+export default setEmoteFromUrl;
diff --git a/webAO/utils/assetCache.ts b/webAO/utils/assetCache.ts
new file mode 100644
index 0000000..7504207
--- /dev/null
+++ b/webAO/utils/assetCache.ts
@@ -0,0 +1,129 @@
+/**
+ * Unified asset resolution and preloading cache.
+ *
+ * Every resolve/preload function caches its promise by URL, so:
+ * - Concurrent calls for the same URL share one in-flight request
+ * - Subsequent calls return instantly from cache
+ * - Results are cached for the entire session (CDN assets don't change mid-session)
+ *
+ * fileExists.ts handles HEAD-request caching independently (same pattern).
+ * This module handles everything above that: URL resolution, image/audio
+ * preloading, and animation duration calculation.
+ */
+
+import findImgSrc from "./findImgSrc";
+import fileExists from "./fileExists";
+import getAnimLength from "./getAnimLength";
+import transparentPng from "../constants/transparentPng";
+
+const PRELOAD_TIMEOUT_MS = 5000;
+const AUDIO_PRELOAD_TIMEOUT_MS = 3000;
+
+/** All cached promises, keyed by a type-prefixed key */
+const cache = new Map<string, Promise<any>>();
+
+// ── Core cache helper ───────────────────────────────────────────────
+
+function cached<T>(key: string, factory: () => Promise<T>): Promise<T> {
+ const existing = cache.get(key);
+ if (existing !== undefined) return existing;
+ const promise = factory();
+ cache.set(key, promise);
+ return promise;
+}
+
+// ── Public API ──────────────────────────────────────────────────────
+
+/**
+ * Resolves which URL from a candidate list exists, then preloads the
+ * image into the browser cache. Returns the resolved URL or transparentPng.
+ *
+ * Cached by the candidate list (stringified), so the same set of
+ * candidates always returns the same resolved+preloaded URL.
+ */
+export function resolveAndPreloadImage(urls: string[]): Promise<string> {
+ const key = `img:${urls.join("|")}`;
+ return cached(key, async () => {
+ const url = await findImgSrc(urls);
+ if (url && url !== transparentPng) {
+ await doPreloadImage(url);
+ }
+ return url;
+ });
+}
+
+/**
+ * Checks if an audio URL exists, then preloads it into the browser cache.
+ * Returns the URL if it exists, or null.
+ */
+export function resolveAndPreloadAudio(url: string): Promise<string | null> {
+ const key = `audio:${url}`;
+ return cached(key, async () => {
+ const exists = await fileExists(url);
+ if (!exists) return null;
+ await doPreloadAudio(url);
+ return url;
+ });
+}
+
+/**
+ * Preloads an audio URL that is already known to exist (no HEAD check).
+ * Useful when the URL comes from a trusted source like client.resources.
+ */
+export function preloadKnownAudio(url: string): Promise<string> {
+ const key = `audio:${url}`;
+ return cached(key, async () => {
+ await doPreloadAudio(url);
+ return url;
+ });
+}
+
+/**
+ * Gets the animation duration for a base URL (tries .gif, .webp, .apng).
+ * Downloads the file to count frames, which also primes the browser cache.
+ */
+export function getAnimDuration(baseUrl: string): Promise<number> {
+ const key = `animdur:${baseUrl}`;
+ return cached(key, () => getAnimLength(baseUrl));
+}
+
+// ── Internal preloaders ─────────────────────────────────────────────
+
+function doPreloadImage(url: string): Promise<void> {
+ return new Promise<void>((resolve) => {
+ const img = new Image();
+ let settled = false;
+
+ const timer = setTimeout(() => {
+ if (!settled) { settled = true; resolve(); }
+ }, PRELOAD_TIMEOUT_MS);
+
+ img.onload = () => {
+ if (!settled) { settled = true; clearTimeout(timer); resolve(); }
+ };
+ img.onerror = () => {
+ if (!settled) { settled = true; clearTimeout(timer); resolve(); }
+ };
+ img.src = url;
+ });
+}
+
+function doPreloadAudio(url: string): Promise<void> {
+ return new Promise<void>((resolve) => {
+ const audio = new Audio();
+ let settled = false;
+
+ const timer = setTimeout(() => {
+ if (!settled) { settled = true; resolve(); }
+ }, AUDIO_PRELOAD_TIMEOUT_MS);
+
+ audio.oncanplaythrough = () => {
+ if (!settled) { settled = true; clearTimeout(timer); resolve(); }
+ };
+ audio.onerror = () => {
+ if (!settled) { settled = true; clearTimeout(timer); resolve(); }
+ };
+ audio.preload = "auto";
+ audio.src = url;
+ });
+}
diff --git a/webAO/utils/fileExists.ts b/webAO/utils/fileExists.ts
index 1dceb72..748bc1f 100644
--- a/webAO/utils/fileExists.ts
+++ b/webAO/utils/fileExists.ts
@@ -1,14 +1,15 @@
-export default async function fileExists(url: string): Promise<boolean> {
- return new Promise((resolve) => {
+const cache = new Map<string, Promise<boolean>>();
+
+export default function fileExists(url: string): Promise<boolean> {
+ const cached = cache.get(url);
+ if (cached !== undefined) return cached;
+
+ const promise = new Promise<boolean>((resolve) => {
const xhr = new XMLHttpRequest();
xhr.open("HEAD", url);
xhr.onload = function checkLoad() {
if (xhr.readyState === 4) {
- if (xhr.status === 200) {
- resolve(true);
- } else {
- resolve(false);
- }
+ resolve(xhr.status === 200);
}
};
xhr.onerror = function checkError() {
@@ -16,4 +17,7 @@ export default async function fileExists(url: string): Promise<boolean> {
};
xhr.send(null);
});
+
+ cache.set(url, promise);
+ return promise;
}
diff --git a/webAO/viewport/interfaces/ChatMsg.ts b/webAO/viewport/interfaces/ChatMsg.ts
index 1e7078f..ab8d0b3 100644
--- a/webAO/viewport/interfaces/ChatMsg.ts
+++ b/webAO/viewport/interfaces/ChatMsg.ts
@@ -1,3 +1,5 @@
+import { PreloadedAssets } from "./PreloadedAssets";
+
export interface ChatMsg {
content: string;
objection: number;
@@ -31,4 +33,5 @@ export interface ChatMsg {
evidence?: number;
looping_sfx?: boolean;
noninterrupting_preanim?: number;
+ preloadedAssets?: PreloadedAssets;
}
diff --git a/webAO/viewport/interfaces/PreloadedAssets.ts b/webAO/viewport/interfaces/PreloadedAssets.ts
new file mode 100644
index 0000000..54d20b8
--- /dev/null
+++ b/webAO/viewport/interfaces/PreloadedAssets.ts
@@ -0,0 +1,20 @@
+export interface PreloadedAssets {
+ /** Resolved URL for idle (a) sprite */
+ idleUrl: string;
+ /** Resolved URL for talking (b) sprite */
+ talkingUrl: string;
+ /** Resolved URL for pre-animation sprite (no prefix) */
+ preanimUrl: string;
+ /** Duration of preanim in ms (0 if no preanim) */
+ preanimDuration: number;
+ /** Resolved URL for paired character idle (a) sprite */
+ pairIdleUrl: string;
+ /** Resolved per-character shout SFX URL, or null to use default */
+ shoutSfxUrl: string | null;
+ /** Resolved emote SFX URL, or null if no sound */
+ emoteSfxUrl: string | null;
+ /** Resolved realization (flash) SFX URL */
+ realizationSfxUrl: string | null;
+ /** Resolved stab (screenshake) SFX URL */
+ stabSfxUrl: string | null;
+}
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 };
+ }
+}
diff --git a/webAO/viewport/viewport.ts b/webAO/viewport/viewport.ts
index aea43f1..45cfc96 100644
--- a/webAO/viewport/viewport.ts
+++ b/webAO/viewport/viewport.ts
@@ -3,6 +3,7 @@
import { client, delay } from "../client";
import { UPDATE_INTERVAL } from "../client";
import setEmote from "../client/setEmote";
+import setEmoteFromUrl from "../client/setEmoteFromUrl";
import { AO_HOST } from "../client/aoHost";
import { Viewport } from "./interfaces/Viewport";
import { createBlipsChannels } from "./utils/createBlipChannels";
@@ -162,7 +163,9 @@ const viewport = (): Viewport => {
const nextCharacterElement = chatmsg.parsed[textnow.length];
const flash = async () => {
const effectlayer = document.getElementById("client_fg");
- playSFX(`${AO_HOST}sounds/general/sfx-realization.opus`, false);
+ const realizationUrl = chatmsg.preloadedAssets?.realizationSfxUrl
+ ?? `${AO_HOST}sounds/general/sfx-realization.opus`;
+ playSFX(realizationUrl, false);
effectlayer.style.animation = "flash 0.4s 1";
await delay(400);
effectlayer.style.removeProperty("animation");
@@ -170,7 +173,9 @@ const viewport = (): Viewport => {
const shake = async () => {
const gamewindow = document.getElementById("client_gamewindow");
- playSFX(`${AO_HOST}sounds/general/sfx-stab.opus`, false);
+ const stabUrl = chatmsg.preloadedAssets?.stabSfxUrl
+ ?? `${AO_HOST}sounds/general/sfx-stab.opus`;
+ playSFX(stabUrl, false);
gamewindow.style.animation = "shake 0.2s 1";
await delay(200);
gamewindow.style.removeProperty("animation");
@@ -247,15 +252,11 @@ const viewport = (): Viewport => {
if (textnow === chatmsg.content) {
animating = false;
- setEmote(
- AO_HOST,
- client,
- charName,
- charEmote,
- "(a)",
- false,
- chatmsg.side,
- );
+ if (chatmsg.preloadedAssets) {
+ setEmoteFromUrl(chatmsg.preloadedAssets.idleUrl, false, chatmsg.side);
+ } else {
+ setEmote(AO_HOST, client, charName, charEmote, "(a)", false, chatmsg.side);
+ }
charLayers.style.opacity = "1";
waitingBox.style.opacity = "1";
clearTimeout(updater);
@@ -332,12 +333,16 @@ const viewport = (): Viewport => {
// Effect stuff
if (chatmsg.screenshake === 1) {
// Shake screen
- playSFX(`${AO_HOST}sounds/general/sfx-stab.opus`, false);
+ const stabUrl = chatmsg.preloadedAssets?.stabSfxUrl
+ ?? `${AO_HOST}sounds/general/sfx-stab.opus`;
+ playSFX(stabUrl, false);
gamewindow.style.animation = "shake 0.2s 1";
}
if (chatmsg.flash === 1) {
// Flash screen
- playSFX(`${AO_HOST}sounds/general/sfx-realization.opus`, false);
+ const realizationUrl = chatmsg.preloadedAssets?.realizationSfxUrl
+ ?? `${AO_HOST}sounds/general/sfx-realization.opus`;
+ playSFX(realizationUrl, false);
effectlayer.style.animation = "flash 0.4s 1";
}
@@ -345,8 +350,12 @@ const viewport = (): Viewport => {
if (chatmsg.preanimdelay > 0) {
shoutSprite.style.display = "none";
shoutSprite.style.animation = "";
- const preanim = chatmsg.preanim.toLowerCase();
- setEmote(AO_HOST, client, charName, preanim, "", false, chatmsg.side);
+ if (chatmsg.preloadedAssets) {
+ setEmoteFromUrl(chatmsg.preloadedAssets.preanimUrl, false, chatmsg.side);
+ } else {
+ const preanim = chatmsg.preanim.toLowerCase();
+ setEmote(AO_HOST, client, charName, preanim, "", false, chatmsg.side);
+ }
}
if (chatmsg.other_name) {
@@ -432,41 +441,29 @@ const viewport = (): Viewport => {
}
if (chatmsg.other_name) {
- setEmote(
- AO_HOST,
- client,
- pairName,
- pairEmote,
- "(a)",
- true,
- chatmsg.side,
- );
+ if (chatmsg.preloadedAssets) {
+ setEmoteFromUrl(chatmsg.preloadedAssets.pairIdleUrl, true, chatmsg.side);
+ } else {
+ setEmote(AO_HOST, client, pairName, pairEmote, "(a)", true, chatmsg.side);
+ }
pairLayers.style.opacity = "1";
} else {
pairLayers.style.opacity = "0";
}
- setEmote(
- AO_HOST,
- client,
- charName,
- charEmote,
- "(b)",
- false,
- chatmsg.side,
- );
+ if (chatmsg.preloadedAssets) {
+ setEmoteFromUrl(chatmsg.preloadedAssets.talkingUrl, false, chatmsg.side);
+ } else {
+ setEmote(AO_HOST, client, charName, charEmote, "(b)", false, chatmsg.side);
+ }
charLayers.style.opacity = "1";
if (textnow === chatmsg.content) {
- setEmote(
- AO_HOST,
- client,
- charName,
- charEmote,
- "(a)",
- false,
- chatmsg.side,
- );
+ if (chatmsg.preloadedAssets) {
+ setEmoteFromUrl(chatmsg.preloadedAssets.idleUrl, false, chatmsg.side);
+ } else {
+ setEmote(AO_HOST, client, charName, charEmote, "(a)", false, chatmsg.side);
+ }
charLayers.style.opacity = "1";
waitingBox.style.opacity = "1";
animating = false;
@@ -491,15 +488,12 @@ const viewport = (): Viewport => {
chatmsg.sound !== undefined &&
(chatmsg.type == 1 || chatmsg.type == 2 || chatmsg.type == 6)
) {
- playSFX(
- `${AO_HOST}sounds/general/${encodeURI(
- chatmsg.sound.toLowerCase(),
- )}.opus`,
- chatmsg.looping_sfx,
- );
+ const sfxUrl = chatmsg.preloadedAssets?.emoteSfxUrl
+ ?? `${AO_HOST}sounds/general/${encodeURI(chatmsg.sound.toLowerCase())}.opus`;
+ playSFX(sfxUrl, chatmsg.looping_sfx);
}
}
- if (textnow === chatmsg.content) {
+ if (textnow === chatmsg.content && !startFirstTickCheck && !startSecondTickCheck) {
return;
}
if (animating) {