diff options
| -rw-r--r-- | .eslintrc.js | 1 | ||||
| -rw-r--r-- | webAO/client/setEmoteFromUrl.ts | 21 | ||||
| -rw-r--r-- | webAO/utils/assetCache.ts | 129 | ||||
| -rw-r--r-- | webAO/utils/fileExists.ts | 18 | ||||
| -rw-r--r-- | webAO/viewport/interfaces/ChatMsg.ts | 3 | ||||
| -rw-r--r-- | webAO/viewport/interfaces/PreloadedAssets.ts | 20 | ||||
| -rw-r--r-- | webAO/viewport/utils/handleICSpeaking.ts | 57 | ||||
| -rw-r--r-- | webAO/viewport/utils/preloadMessageAssets.ts | 151 | ||||
| -rw-r--r-- | webAO/viewport/viewport.ts | 92 |
9 files changed, 399 insertions, 93 deletions
diff --git a/.eslintrc.js b/.eslintrc.js index 99e93ae..7335916 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,5 +17,6 @@ module.exports = { rules: { "indent": ["warn", 2, { "SwitchCase": 1 }], "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }], + "@typescript-eslint/no-explicit-any": "off", }, }; 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) { |
