From 108636666d474119892c4b3a2f3beadb767b006e Mon Sep 17 00:00:00 2001 From: Caleb Date: Sat, 10 Sep 2022 11:09:49 -0400 Subject: Structured viewport a little differently --- tsconfig.json | 2 +- webAO/client.ts | 3 +- webAO/client/loadResources.ts | 4 +- webAO/dom/changeBlipVolume.ts | 15 + webAO/viewport.ts | 1092 -------------------------- webAO/viewport/constants/colors.ts | 11 + webAO/viewport/constants/defaultChatMsg.ts | 15 + webAO/viewport/constants/positions.ts | 45 ++ webAO/viewport/constants/shouts.ts | 1 + webAO/viewport/interfaces/ChatMsg.ts | 34 + webAO/viewport/interfaces/Desk.ts | 4 + webAO/viewport/interfaces/Position.ts | 7 + webAO/viewport/interfaces/Positions.ts | 5 + webAO/viewport/interfaces/Testimony.ts | 3 + webAO/viewport/interfaces/Viewport.ts | 24 + webAO/viewport/utils/createBlipChannels.ts | 15 + webAO/viewport/utils/createMusic.ts | 13 + webAO/viewport/utils/createSfxAudio.ts | 9 + webAO/viewport/utils/createShoutAudio.ts | 9 + webAO/viewport/utils/createTestimonyAudio.ts | 9 + webAO/viewport/viewport.ts | 901 +++++++++++++++++++++ 21 files changed, 1125 insertions(+), 1096 deletions(-) create mode 100644 webAO/dom/changeBlipVolume.ts delete mode 100644 webAO/viewport.ts create mode 100644 webAO/viewport/constants/colors.ts create mode 100644 webAO/viewport/constants/defaultChatMsg.ts create mode 100644 webAO/viewport/constants/positions.ts create mode 100644 webAO/viewport/constants/shouts.ts create mode 100644 webAO/viewport/interfaces/ChatMsg.ts create mode 100644 webAO/viewport/interfaces/Desk.ts create mode 100644 webAO/viewport/interfaces/Position.ts create mode 100644 webAO/viewport/interfaces/Positions.ts create mode 100644 webAO/viewport/interfaces/Testimony.ts create mode 100644 webAO/viewport/interfaces/Viewport.ts create mode 100644 webAO/viewport/utils/createBlipChannels.ts create mode 100644 webAO/viewport/utils/createMusic.ts create mode 100644 webAO/viewport/utils/createSfxAudio.ts create mode 100644 webAO/viewport/utils/createShoutAudio.ts create mode 100644 webAO/viewport/utils/createTestimonyAudio.ts create mode 100644 webAO/viewport/viewport.ts diff --git a/tsconfig.json b/tsconfig.json index 9422b00..1a2dd33 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,5 +8,5 @@ "strictNullChecks": false, //document.getElementBy "downlevelIteration": true }, - "include": ["./webAO/*"] + "include": ["./webAO/*", "webAO/viewport/viewport.ts"] } \ No newline at end of file diff --git a/webAO/client.ts b/webAO/client.ts index 8b2f691..b908b62 100644 --- a/webAO/client.ts +++ b/webAO/client.ts @@ -8,7 +8,8 @@ import FingerprintJS from "@fingerprintjs/fingerprintjs"; import { sender, ISender } from './client/sender/index' import queryParser from "./utils/queryParser"; import getResources from "./utils/getResources.js"; -import masterViewport, { Viewport } from "./viewport"; +import masterViewport from "./viewport/viewport"; +import { Viewport } from './viewport/interfaces/Viewport'; import { EventEmitter } from "events"; import { onReplayGo } from './dom/onReplayGo' import { packetHandler } from './packets/packetHandler' diff --git a/webAO/client/loadResources.ts b/webAO/client/loadResources.ts index 7039333..65d0895 100644 --- a/webAO/client/loadResources.ts +++ b/webAO/client/loadResources.ts @@ -5,7 +5,7 @@ import { client } from "../client"; import { setChatbox } from "../dom/setChatbox"; import { changeSFXVolume, changeShoutVolume, changeTestimonyVolume } from "../dom/changeVolume"; import { showname_click } from "../dom/showNameClick"; - +import { changeBlipVolume } from '../dom/changeBlipVolume' const version = process.env.npm_package_version; /** * Load game resources and stored settings. @@ -65,7 +65,7 @@ export const loadResources = () => { changeTestimonyVolume(); (document.getElementById("client_bvolume")).value = getCookie("blipVolume") || "1"; - client.viewport.changeBlipVolume(); + changeBlipVolume(); (document.getElementById("ic_chat_name")).value = getCookie("ic_chat_name"); diff --git a/webAO/dom/changeBlipVolume.ts b/webAO/dom/changeBlipVolume.ts new file mode 100644 index 0000000..572f389 --- /dev/null +++ b/webAO/dom/changeBlipVolume.ts @@ -0,0 +1,15 @@ +import setCookie from "../utils/setCookie"; +import { client } from '../client' +/** + * Triggered by the blip volume slider. + */ +export const changeBlipVolume = () => { + const blipVolume = (( + document.getElementById("client_bvolume") + )).value; + client.viewport.blipChannels.forEach( + (channel: HTMLAudioElement) => (channel.volume = Number(blipVolume)) + ); + setCookie("blipVolume", blipVolume); +} +window.changeBlipVolume = changeBlipVolume; diff --git a/webAO/viewport.ts b/webAO/viewport.ts deleted file mode 100644 index 7716409..0000000 --- a/webAO/viewport.ts +++ /dev/null @@ -1,1092 +0,0 @@ -import tryUrls from "./utils/tryUrls"; -import fileExists from "./utils/fileExists"; -import Client, { delay } from "./client"; -import { opusCheck } from './dom/opusCheck' -import { UPDATE_INTERVAL } from "./client"; -import { setChatbox } from "./dom/setChatbox"; -import { resizeChatbox } from "./dom/resizeChatbox"; -import transparentPng from "./constants/transparentPng"; -import mlConfig from "./utils/aoml"; -import setEmote from "./client/setEmote"; -import getAnimLength from "./utils/getAnimLength"; -import { safeTags } from "./encoding"; -import setCookie from "./utils/setCookie"; -import { AO_HOST } from "./client/aoHost"; -import { appendICLog } from "./client/appendICLog"; -import { checkCallword } from './client/checkCallword' - -interface ChatMsg { - content: string; - objection: number; - sound: string; - startpreanim: boolean; - startspeaking: boolean; - side: any; - color: number; - snddelay: number; - preanimdelay: number; - speed: number; - blips: string; - self_offset?: number[]; - other_offset?: number[]; - showname?: string; - nameplate?: string; - flip?: number; - other_flip?: number; - effects?: string[]; - deskmod?: number; - preanim?: string; - other_name?: string; - sprite?: string; - name?: string; - chatbox?: string; - other_emote?: string; - parsed?: HTMLSpanElement[]; - screenshake?: number; - flash?: number; - type?: number; - evidence?: number; - looping_sfx?: boolean; - noninterrupting_preanim?: number; -} -interface Testimony { - [key: number]: string; -} -export interface Viewport { - chat_tick: Function; - changeMusicVolume: Function; - changeBlipVolume: Function; - reloadTheme: Function; - playSFX: Function; - set_side: Function; - initTestimonyUpdater: Function; - updateTestimony: Function; - disposeTestimony: Function; - handle_ic_speaking: Function; - handleTextTick: Function; - theme: string; - chatmsg: ChatMsg; - setSfxAudio: Function; - getSfxAudio: Function; - getBackgroundFolder: Function; - blipChannels: HTMLAudioElement[]; - music: any; - musicVolume: number; - setBackgroundName: Function; - lastChar: string; - getBackgroundName: Function; -} -const SHOUTS = [undefined, "holdit", "objection", "takethat", "custom"]; - -const COLORS = [ - "white", - "green", - "red", - "orange", - "blue", - "yellow", - "pink", - "cyan", - "grey", -]; -const createMusic = () => { - const audioChannels = document.getElementsByClassName( - "audioChannel" - ) as HTMLCollectionOf; - let music = [...audioChannels]; - music.forEach((channel: HTMLAudioElement) => (channel.volume = 0.5)); - music.forEach( - (channel: HTMLAudioElement) => (channel.onerror = opusCheck(channel)) - ); - return music; -}; -const createTestimonyAudio = () => { - const testimonyAudio = document.getElementById( - "client_testimonyaudio" - ) as HTMLAudioElement; - testimonyAudio.src = `${AO_HOST}sounds/general/sfx-guilty.opus`; - return testimonyAudio; -}; - -const createShoutAudio = () => { - const shoutAudio = document.getElementById( - "client_shoutaudio" - ) as HTMLAudioElement; - shoutAudio.src = `${AO_HOST}misc/default/objection.opus`; - return shoutAudio; -}; -const createSfxAudio = () => { - const sfxAudio = document.getElementById( - "client_sfxaudio" - ) as HTMLAudioElement; - sfxAudio.src = `${AO_HOST}sounds/general/sfx-realization.opus`; - return sfxAudio; -}; -const createBlipsChannels = () => { - const blipSelectors = document.getElementsByClassName( - "blipSound" - ) as HTMLCollectionOf; - - const blipChannels = [...blipSelectors]; - // Allocate multiple blip audio channels to make blips less jittery - blipChannels.forEach((channel: HTMLAudioElement) => (channel.volume = 0.5)); - blipChannels.forEach( - (channel: HTMLAudioElement) => (channel.onerror = opusCheck(channel)) - ); - return blipChannels; -}; -const defaultChatMsg = { - content: "", - objection: 0, - sound: "", - startpreanim: true, - startspeaking: false, - side: null, - color: 0, - snddelay: 0, - preanimdelay: 0, - speed: UPDATE_INTERVAL, -} as ChatMsg; -interface Desk { - ao2?: string; - ao1?: string; -} -interface Position { - bg?: string; - desk?: Desk; - speedLines: string; -} - -interface Positions { - [key: string]: Position; -} - -const positions: Positions = { - def: { - bg: "defenseempty", - desk: { ao2: "defensedesk.png", ao1: "bancodefensa.png" } as Desk, - speedLines: "defense_speedlines.gif", - }, - pro: { - bg: "prosecutorempty", - desk: { ao2: "prosecutiondesk.png", ao1: "bancoacusacion.png" } as Desk, - speedLines: "prosecution_speedlines.gif", - }, - hld: { - bg: "helperstand", - desk: null as Desk, - speedLines: "defense_speedlines.gif", - }, - hlp: { - bg: "prohelperstand", - desk: null as Desk, - speedLines: "prosecution_speedlines.gif", - }, - wit: { - bg: "witnessempty", - desk: { ao2: "stand.png", ao1: "estrado.png" } as Desk, - speedLines: "prosecution_speedlines.gif", - }, - jud: { - bg: "judgestand", - desk: { ao2: "judgedesk.png", ao1: "judgedesk.gif" } as Desk, - speedLines: "prosecution_speedlines.gif", - }, - jur: { - bg: "jurystand", - desk: { ao2: "jurydesk.png", ao1: "estrado.png" } as Desk, - speedLines: "defense_speedlines.gif", - }, - sea: { - bg: "seancestand", - desk: { ao2: "seancedesk.png", ao1: "estrado.png" } as Desk, - speedLines: "prosecution_speedlines.gif", - }, -}; -const viewport = (masterClient: Client): Viewport => { - let animating = false; - let attorneyMarkdown = mlConfig(AO_HOST); - let blipChannels = createBlipsChannels(); - let chatmsg = defaultChatMsg; - let client = masterClient; - let currentBlipChannel = 0; - let lastChar = ""; - let lastEvi = 0; - let music = createMusic(); - let musicVolume = 0; - let sfxAudio = createSfxAudio(); - let sfxplayed = 0; - let shoutTimer = 0; - let shoutaudio = createShoutAudio(); - let startFirstTickCheck: boolean; - let startSecondTickCheck: boolean; - let startThirdTickCheck: boolean; - let testimonyAudio = createTestimonyAudio(); - let testimonyTimer = 0; - let testimonyUpdater: any; - let textnow = ""; - let theme: string; - let tickTimer = 0; - let updater: any; - let backgroundName = ""; - const getSfxAudio = () => sfxAudio; - const setSfxAudio = (value: HTMLAudioElement) => (sfxAudio = value); - const getBackgroundName = () => backgroundName; - const setBackgroundName = (value: string) => (backgroundName = value); - const getBackgroundFolder = () => - `${AO_HOST}background/${encodeURI(backgroundName.toLowerCase())}/`; - - const playSFX = async (sfxname: string, looping: boolean) => { - sfxAudio.pause(); - sfxAudio.loop = looping; - sfxAudio.src = sfxname; - sfxAudio.play(); - }; - - /** - * Changes the viewport background based on a given position. - * - * Valid positions: `def, pro, hld, hlp, wit, jud, jur, sea` - * @param {string} position the position to change into - */ - const set_side = async ({ - position, - showSpeedLines, - showDesk, - }: { - position: string; - showSpeedLines: boolean; - showDesk: boolean; - }) => { - const view = document.getElementById("client_fullview"); - - let bench: HTMLImageElement; - if ("def,pro,wit".includes(position)) { - bench = ( - document.getElementById(`client_${position}_bench`) - ); - } else { - bench = document.getElementById("client_bench_classic"); - } - - let court: HTMLImageElement; - if ("def,pro,wit".includes(position)) { - court = ( - document.getElementById(`client_court_${position}`) - ); - } else { - court = document.getElementById("client_court_classic"); - } - - let bg; - let desk; - let speedLines; - - if ("def,pro,hld,hlp,wit,jud,jur,sea".includes(position)) { - bg = positions[position].bg; - desk = positions[position].desk; - speedLines = positions[position].speedLines; - } else { - bg = `${position}`; - desk = { ao2: `${position}_overlay.png`, ao1: "_overlay.png" }; - speedLines = "defense_speedlines.gif"; - } - - if (showSpeedLines === true) { - court.src = `${AO_HOST}themes/default/${encodeURI(speedLines)}`; - } else { - court.src = await tryUrls(getBackgroundFolder() + bg); - } - - if (showDesk === true && desk) { - const deskFilename = (await fileExists(getBackgroundFolder() + desk.ao2)) - ? desk.ao2 - : desk.ao1; - bench.src = getBackgroundFolder() + deskFilename; - bench.style.opacity = "1"; - } else { - bench.style.opacity = "0"; - } - - if ("def,pro,wit".includes(position)) { - view.style.display = ""; - document.getElementById("client_classicview").style.display = "none"; - switch (position) { - case "def": - view.style.left = "0"; - break; - case "wit": - view.style.left = "-200%"; - break; - case "pro": - view.style.left = "-400%"; - break; - } - } else { - view.style.display = "none"; - document.getElementById("client_classicview").style.display = ""; - } - }; - - /** - * Intialize testimony updater - */ - const initTestimonyUpdater = () => { - const testimonyFilenames: Testimony = { - 1: "witnesstestimony", - 2: "crossexamination", - 3: "notguilty", - 4: "guilty", - }; - - const testimony = testimonyFilenames[masterClient.testimonyID]; - if (!testimony) { - console.warn(`Invalid testimony ID ${masterClient.testimonyID}`); - return; - } - - testimonyAudio.src = masterClient.resources[testimony].sfx; - testimonyAudio.play(); - - const testimonyOverlay = ( - document.getElementById("client_testimony") - ); - testimonyOverlay.src = masterClient.resources[testimony].src; - testimonyOverlay.style.opacity = "1"; - - testimonyTimer = 0; - testimonyUpdater = setTimeout(() => updateTestimony(), UPDATE_INTERVAL); - }; - - /** - * Updates the testimony overaly - */ - const updateTestimony = () => { - const testimonyFilenames: Testimony = { - 1: "witnesstestimony", - 2: "crossexamination", - 3: "notguilty", - 4: "guilty", - }; - - // Update timer - testimonyTimer += UPDATE_INTERVAL; - - const testimony = testimonyFilenames[masterClient.testimonyID]; - const resource = masterClient.resources[testimony]; - if (!resource) { - disposeTestimony(); - return; - } - - if (testimonyTimer >= resource.duration) { - disposeTestimony(); - } else { - testimonyUpdater = setTimeout(() => updateTestimony(), UPDATE_INTERVAL); - } - }; - - /** - * Dispose the testimony overlay - */ - const disposeTestimony = () => { - masterClient.testimonyID = 0; - testimonyTimer = 0; - document.getElementById("client_testimony").style.opacity = "0"; - clearTimeout(testimonyUpdater); - }; - - /** - * Sets a new emote. - * 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 - */ - const handle_ic_speaking = async (playerChatMsg: ChatMsg) => { - chatmsg = playerChatMsg; - client.viewport.chatmsg = playerChatMsg; - - textnow = ""; - sfxplayed = 0; - tickTimer = 0; - animating = true; - startFirstTickCheck = true; - startSecondTickCheck = false; - startThirdTickCheck = false; - let charLayers = document.getElementById("client_char"); - let pairLayers = document.getElementById("client_pair_char"); - // stop updater - clearTimeout(updater); - - // stop last sfx from looping any longer - sfxAudio.loop = false; - - const fg = document.getElementById("client_fg"); - const gamewindow = document.getElementById("client_gamewindow"); - const waitingBox = document.getElementById("client_chatwaiting"); - - // Reset CSS animation - gamewindow.style.animation = ""; - waitingBox.style.opacity = "0"; - - const eviBox = document.getElementById("client_evi"); - - if (lastEvi !== chatmsg.evidence) { - eviBox.style.opacity = "0"; - eviBox.style.height = "0%"; - } - lastEvi = chatmsg.evidence; - - const validSides: string[] = ["def", "pro", "wit"]; // these are for the full view pan, the other positions use 'client_char' - if (validSides.includes(chatmsg.side)) { - charLayers = document.getElementById(`client_${chatmsg.side}_char`); - pairLayers = document.getElementById(`client_${chatmsg.side}_pair_char`); - } - - const chatContainerBox = document.getElementById("client_chatcontainer"); - const nameBoxInner = document.getElementById("client_inner_name"); - const chatBoxInner = document.getElementById("client_inner_chat"); - - const displayname = - (document.getElementById("showname")).checked && - chatmsg.showname !== "" - ? chatmsg.showname - : chatmsg.nameplate; - - // Clear out the last message - chatBoxInner.innerText = textnow; - nameBoxInner.innerText = displayname; - - if (lastChar !== chatmsg.name) { - charLayers.style.opacity = "0"; - pairLayers.style.opacity = "0"; - } - - lastChar = chatmsg.name; - client.viewport.lastChar = chatmsg.name; - - appendICLog(chatmsg.content, chatmsg.showname, chatmsg.nameplate); - - checkCallword(chatmsg.content, sfxAudio); - - setEmote( - AO_HOST, - client, - chatmsg.name.toLowerCase(), - chatmsg.sprite, - "(a)", - false, - chatmsg.side - ); - - if (chatmsg.other_name) { - setEmote( - AO_HOST, - client, - chatmsg.other_name.toLowerCase(), - chatmsg.other_emote, - "(a)", - false, - chatmsg.side - ); - } - - // gets which shout shall played - const shoutSprite = ( - document.getElementById("client_shout") - ); - const shout = SHOUTS[chatmsg.objection]; - if (shout) { - // Hide message box - chatContainerBox.style.opacity = "0"; - if (chatmsg.objection === 4) { - shoutSprite.src = `${AO_HOST}characters/${encodeURI( - chatmsg.name.toLowerCase() - )}/custom.gif`; - } else { - shoutSprite.src = masterClient.resources[shout].src; - shoutSprite.style.animation = "bubble 700ms steps(10, jump-both)"; - } - shoutSprite.style.opacity = "1"; - - shoutaudio.src = `${AO_HOST}characters/${encodeURI( - chatmsg.name.toLowerCase() - )}/${shout}.opus`; - shoutaudio.play(); - shoutTimer = masterClient.resources[shout].duration; - } else { - shoutTimer = 0; - } - - chatmsg.startpreanim = true; - let gifLength = 0; - - if (chatmsg.type === 1 && chatmsg.preanim !== "-") { - //we have a preanim - chatContainerBox.style.opacity = "0"; - gifLength = await getAnimLength( - `${AO_HOST}characters/${encodeURI( - chatmsg.name.toLowerCase() - )}/${encodeURI(chatmsg.preanim)}` - ); - console.debug("preanim is " + gifLength + " long"); - chatmsg.startspeaking = false; - } else { - chatmsg.startspeaking = true; - if (chatmsg.content !== "") chatContainerBox.style.opacity = "1"; - } - chatmsg.preanimdelay = gifLength; - const setAside = { - position: chatmsg.side, - showSpeedLines: false, - showDesk: false, - }; - let skipoffset: boolean = false; - if (chatmsg.type === 5) { - setAside.showSpeedLines = true; - setAside.showDesk = false; - set_side(setAside); - } else { - switch (Number(chatmsg.deskmod)) { - case 0: //desk is hidden - setAside.showSpeedLines = false; - setAside.showDesk = false; - set_side(setAside); - break; - case 1: //desk is shown - setAside.showSpeedLines = false; - setAside.showDesk = true; - set_side(setAside); - break; - case 2: //desk is hidden during preanim, but shown during idle/talk - setAside.showSpeedLines = false; - setAside.showDesk = false; - set_side(setAside); - break; - case 3: //opposite of 2 - setAside.showSpeedLines = false; - setAside.showDesk = false; - set_side(setAside); - break; - case 4: //desk is hidden, character offset is ignored, pair character is hidden during preanim, normal behavior during idle/talk - setAside.showSpeedLines = false; - setAside.showDesk = false; - set_side(setAside); - skipoffset = true; - break; - case 5: //opposite of 4 - setAside.showSpeedLines = false; - setAside.showDesk = true; - set_side(setAside); - break; - default: - setAside.showSpeedLines = false; - setAside.showDesk = true; - set_side(setAside); - break; - } - } - - setChatbox(chatmsg.chatbox); - resizeChatbox(); - - if (!skipoffset) { - // Flip the character - charLayers.style.transform = - chatmsg.flip === 1 ? "scaleX(-1)" : "scaleX(1)"; - pairLayers.style.transform = - chatmsg.other_flip === 1 ? "scaleX(-1)" : "scaleX(1)"; - - // Shift by the horizontal offset - switch (chatmsg.side) { - case "wit": - pairLayers.style.left = `${200 + Number(chatmsg.other_offset[0])}%`; - charLayers.style.left = `${200 + Number(chatmsg.self_offset[0])}%`; - break; - case "pro": - pairLayers.style.left = `${400 + Number(chatmsg.other_offset[0])}%`; - charLayers.style.left = `${400 + Number(chatmsg.self_offset[0])}%`; - break; - default: - pairLayers.style.left = `${Number(chatmsg.other_offset[0])}%`; - charLayers.style.left = `${Number(chatmsg.self_offset[0])}%`; - break; - } - - // New vertical offsets - pairLayers.style.top = `${Number(chatmsg.other_offset[1])}%`; - charLayers.style.top = `${Number(chatmsg.self_offset[1])}%`; - } - - blipChannels.forEach( - (channel: HTMLAudioElement) => - (channel.src = `${AO_HOST}sounds/general/sfx-blip${encodeURI( - chatmsg.blips.toLowerCase() - )}.opus`) - ); - - // process markup - if (chatmsg.content.startsWith("~~")) { - chatBoxInner.style.textAlign = "center"; - chatmsg.content = chatmsg.content.substring(2, chatmsg.content.length); - } else { - chatBoxInner.style.textAlign = "inherit"; - } - - // apply effects - fg.style.animation = ""; - const effectName = chatmsg.effects[0].toLowerCase(); - const badEffects = ["", "-", "none"]; - if (effectName.startsWith("rain")) { - (document.getElementById("effect_css")).href = "styles/effects/rain.css"; - let intensity = 200; - if (effectName.endsWith("weak")) { - intensity = 100; - } else if (effectName.endsWith("strong")) { - intensity = 400; - } - if (intensity < fg.childElementCount) - fg.innerHTML = ''; - else - intensity = intensity - fg.childElementCount; - - for (let i = 0; i < intensity; i++) { - let drop = document.createElement("p"); - drop.style.left = (Math.random() * 100) + "%"; - drop.style.animationDelay = String(Math.random()) + "s"; - fg.appendChild(drop) - } - } else if ( - chatmsg.effects[0] && - !badEffects.includes(effectName) - ) { - (document.getElementById("effect_css")).href = ""; - fg.innerHTML = ''; - const baseEffectUrl = `${AO_HOST}themes/default/effects/`; - fg.src = `${baseEffectUrl}${encodeURI(effectName)}.webp`; - } else { - fg.innerHTML = ''; - fg.src = transparentPng; - } - - charLayers.style.opacity = "1"; - - const soundChecks = ["0", "1", "", undefined]; - if (soundChecks.some((check) => chatmsg.sound === check)) { - chatmsg.sound = chatmsg.effects[2]; - } - chatmsg.parsed = await attorneyMarkdown.applyMarkdown( - chatmsg.content, - COLORS[chatmsg.color] - ); - chat_tick(); - }; - - const handleTextTick = async (charLayers: HTMLImageElement) => { - const chatBox = document.getElementById("client_chat"); - const waitingBox = document.getElementById("client_chatwaiting"); - const chatBoxInner = document.getElementById("client_inner_chat"); - const charName = chatmsg.name.toLowerCase(); - const charEmote = chatmsg.sprite.toLowerCase(); - - if (chatmsg.content.charAt(textnow.length) !== " ") { - blipChannels[currentBlipChannel].play(); - currentBlipChannel++; - currentBlipChannel %= blipChannels.length; - } - textnow = chatmsg.content.substring(0, textnow.length + 1); - const characterElement = chatmsg.parsed[textnow.length - 1]; - if (characterElement) { - const COMMAND_IDENTIFIER = "\\"; - - const nextCharacterElement = chatmsg.parsed[textnow.length]; - const flash = async () => { - const effectlayer = document.getElementById("client_fg"); - playSFX(`${AO_HOST}sounds/general/sfx-realization.opus`, false); - effectlayer.style.animation = "flash 0.4s 1"; - await delay(400); - effectlayer.style.removeProperty("animation"); - }; - - const shake = async () => { - const gamewindow = document.getElementById("client_gamewindow"); - playSFX(`${AO_HOST}sounds/general/sfx-stab.opus`, false); - gamewindow.style.animation = "shake 0.2s 1"; - await delay(200); - gamewindow.style.removeProperty("animation"); - }; - - const commands = new Map( - Object.entries({ - s: shake, - f: flash, - }) - ); - const textSpeeds = new Set(["{", "}"]); - - // Changing Text Speed - if (textSpeeds.has(characterElement.innerHTML)) { - // Grab them all in a row - const MAX_SLOW_CHATSPEED = 120; - for (let i = textnow.length; i < chatmsg.content.length; i++) { - const currentCharacter = chatmsg.parsed[i - 1].innerHTML; - if (currentCharacter === "}") { - if (chatmsg.speed > 0) { - chatmsg.speed -= 20; - } - } else if (currentCharacter === "{") { - if (chatmsg.speed < MAX_SLOW_CHATSPEED) { - chatmsg.speed += 20; - } - } else { - // No longer at a speed character - textnow = chatmsg.content.substring(0, i); - break; - } - } - } - - if ( - characterElement.innerHTML === COMMAND_IDENTIFIER && - commands.has(nextCharacterElement?.innerHTML) - ) { - textnow = chatmsg.content.substring(0, textnow.length + 1); - await commands.get(nextCharacterElement.innerHTML)(); - } else { - chatBoxInner.appendChild(chatmsg.parsed[textnow.length - 1]); - } - } - // scroll to bottom - chatBox.scrollTop = chatBox.scrollHeight; - - if (textnow === chatmsg.content) { - animating = false; - setEmote( - AO_HOST, - client, - charName, - charEmote, - "(a)", - false, - chatmsg.side - ); - charLayers.style.opacity = "1"; - waitingBox.style.opacity = "1"; - clearTimeout(updater); - } - }; - /** - * Updates the chatbox based on the given text. - * - * OK, here's the documentation on how this works: - * - * 1 _animating - * If we're not done with this characters animation, i.e. his text isn't fully there, set a timeout for the next tick/step to happen - * - * 2 startpreanim - * If the shout timer is over it starts with the preanim - * The first thing it checks for is the shake effect (TODO on client this is handled by the @ symbol and not a flag ) - * Then is the flash/realization effect - * After that, the shout image set to be transparent - * and the main characters preanim gif is loaded - * If pairing is supported the paired character will just stand around with his idle sprite - * - * 3 preanimdelay over - * this animates the evidence popup and finally shows the character name and message box - * it sets the text color and the character speaking sprite - * - * 4 textnow != content - * this adds a character to the textbox and stops the animations if the entire message is present in the textbox - * - * 5 sfx - * independent of the stuff above, this will play any sound effects specified by the emote the character sent. - * happens after the shout delay + an sfx delay that comes with the message packet - * - * XXX: This relies on a global variable `chatmsg`! - */ - const chat_tick = async () => { - // note: this is called fairly often - // do not perform heavy operations here - - await delay(chatmsg.speed); - if (textnow === chatmsg.content) { - return; - } - - const gamewindow = document.getElementById("client_gamewindow"); - const waitingBox = document.getElementById("client_chatwaiting"); - const eviBox = document.getElementById("client_evi"); - const shoutSprite = ( - document.getElementById("client_shout") - ); - const effectlayer = document.getElementById("client_fg"); - const chatBoxInner = document.getElementById("client_inner_chat"); - let charLayers = document.getElementById("client_char"); - let pairLayers = ( - document.getElementById("client_pair_char") - ); - - const validSides: string[] = ["def", "pro", "wit"]; // these are for the full view pan, the other positions use 'client_char' - if (validSides.includes(chatmsg.side)) { - charLayers = ( - document.getElementById(`client_${chatmsg.side}_char`) - ); - pairLayers = ( - document.getElementById(`client_${chatmsg.side}_pair_char`) - ); - } - - const charName = chatmsg.name.toLowerCase(); - const charEmote = chatmsg.sprite.toLowerCase(); - - const pairName = chatmsg.other_name.toLowerCase(); - const pairEmote = chatmsg.other_emote.toLowerCase(); - - // TODO: preanims sometimes play when they're not supposed to - const isShoutOver = tickTimer >= shoutTimer; - const isShoutAndPreanimOver = - tickTimer >= shoutTimer + chatmsg.preanimdelay; - if (isShoutOver && startFirstTickCheck) { - // Effect stuff - if (chatmsg.screenshake === 1) { - // Shake screen - playSFX(`${AO_HOST}sounds/general/sfx-stab.opus`, false); - gamewindow.style.animation = "shake 0.2s 1"; - } - if (chatmsg.flash === 1) { - // Flash screen - playSFX(`${AO_HOST}sounds/general/sfx-realization.opus`, false); - effectlayer.style.animation = "flash 0.4s 1"; - } - - // Pre-animation stuff - if (chatmsg.preanimdelay > 0) { - shoutSprite.style.opacity = "0"; - shoutSprite.style.animation = ""; - const preanim = chatmsg.preanim.toLowerCase(); - setEmote(AO_HOST, client, charName, preanim, "", false, chatmsg.side); - } - - if (chatmsg.other_name) { - pairLayers.style.opacity = "1"; - } else { - pairLayers.style.opacity = "0"; - } - // Done with first check, move to second - startFirstTickCheck = false; - startSecondTickCheck = true; - - chatmsg.startpreanim = false; - chatmsg.startspeaking = true; - } - - const hasNonInterruptingPreAnim = chatmsg.noninterrupting_preanim === 1; - if (textnow !== chatmsg.content && hasNonInterruptingPreAnim) { - const chatContainerBox = document.getElementById("client_chatcontainer"); - chatContainerBox.style.opacity = "1"; - await handleTextTick(charLayers); - } else if (isShoutAndPreanimOver && startSecondTickCheck) { - if (chatmsg.startspeaking) { - chatmsg.startspeaking = false; - - // Evidence Bullshit - if (chatmsg.evidence > 0) { - // Prepare evidence - eviBox.src = safeTags( - masterClient.evidences[chatmsg.evidence - 1].icon - ); - - eviBox.style.width = "auto"; - eviBox.style.height = "36.5%"; - eviBox.style.opacity = "1"; - - testimonyAudio.src = `${AO_HOST}sounds/general/sfx-evidenceshoop.opus`; - testimonyAudio.play(); - - if (chatmsg.side === "def") { - // Only def show evidence on right - eviBox.style.right = "1em"; - eviBox.style.left = "initial"; - } else { - eviBox.style.right = "initial"; - eviBox.style.left = "1em"; - } - } - chatBoxInner.className = `text_${COLORS[chatmsg.color]}`; - - if (chatmsg.preanimdelay === 0) { - shoutSprite.style.opacity = "0"; - shoutSprite.style.animation = ""; - } - - switch (Number(chatmsg.deskmod)) { - case 2: - set_side({ - position: chatmsg.side, - showSpeedLines: false, - showDesk: true, - }); - break; - case 3: - set_side({ - position: chatmsg.side, - showSpeedLines: false, - showDesk: false, - }); - break; - case 4: - set_side({ - position: chatmsg.side, - showSpeedLines: false, - showDesk: true, - }); - break; - case 5: - set_side({ - position: chatmsg.side, - showSpeedLines: false, - showDesk: false, - }); - break; - } - - if (chatmsg.other_name) { - 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 - ); - charLayers.style.opacity = "1"; - - if (textnow === chatmsg.content) { - setEmote( - AO_HOST, - client, - charName, - charEmote, - "(a)", - false, - chatmsg.side - ); - charLayers.style.opacity = "1"; - waitingBox.style.opacity = "1"; - animating = false; - clearTimeout(updater); - return; - } - } else if (textnow !== chatmsg.content) { - const chatContainerBox = document.getElementById( - "client_chatcontainer" - ); - chatContainerBox.style.opacity = "1"; - await handleTextTick(charLayers); - } - } - - if (!sfxplayed && chatmsg.snddelay + shoutTimer >= tickTimer) { - sfxplayed = 1; - if ( - chatmsg.sound !== "0" && - chatmsg.sound !== "1" && - chatmsg.sound !== "" && - 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 - ); - } - } - if (animating) { - chat_tick(); - } - tickTimer += UPDATE_INTERVAL; - }; - /** - * Triggered by the theme selector. - */ - function reloadTheme() { - theme = (document.getElementById("client_themeselect")) - .value; - - setCookie("theme", theme); - (( - document.getElementById("client_theme") - )).href = `styles/${theme}.css`; - } - window.reloadTheme = reloadTheme; - /** - * Triggered by the blip volume slider. - */ - function changeBlipVolume() { - const blipVolume = (( - document.getElementById("client_bvolume") - )).value; - blipChannels.forEach( - (channel: HTMLAudioElement) => (channel.volume = Number(blipVolume)) - ); - setCookie("blipVolume", blipVolume); - } - window.changeBlipVolume = changeBlipVolume; - - const changeMusicVolume = (volume: number = -1) => { - const clientVolume = Number( - (document.getElementById("client_mvolume")).value - ); - let musicVolume = volume === -1 ? clientVolume : volume; - music.forEach( - (channel: HTMLAudioElement) => (channel.volume = musicVolume) - ); - setCookie("musicVolume", String(musicVolume)); - }; - window.changeMusicVolume = changeMusicVolume; - - return { - chat_tick, - changeMusicVolume, - changeBlipVolume, - reloadTheme, - playSFX, - set_side, - setBackgroundName, - initTestimonyUpdater, - updateTestimony, - disposeTestimony, - handle_ic_speaking, - handleTextTick, - getBackgroundFolder, - getBackgroundName, - getSfxAudio, - setSfxAudio, - theme, - chatmsg, - blipChannels, - lastChar, - music, - musicVolume, - }; -}; - -export default viewport; diff --git a/webAO/viewport/constants/colors.ts b/webAO/viewport/constants/colors.ts new file mode 100644 index 0000000..aad3530 --- /dev/null +++ b/webAO/viewport/constants/colors.ts @@ -0,0 +1,11 @@ +export const COLORS = [ + "white", + "green", + "red", + "orange", + "blue", + "yellow", + "pink", + "cyan", + "grey", + ]; \ No newline at end of file diff --git a/webAO/viewport/constants/defaultChatMsg.ts b/webAO/viewport/constants/defaultChatMsg.ts new file mode 100644 index 0000000..8a5db6b --- /dev/null +++ b/webAO/viewport/constants/defaultChatMsg.ts @@ -0,0 +1,15 @@ +import { UPDATE_INTERVAL } from "../../client"; +import { ChatMsg } from "../interfaces/ChatMsg"; + +export const defaultChatMsg = { + content: "", + objection: 0, + sound: "", + startpreanim: true, + startspeaking: false, + side: null, + color: 0, + snddelay: 0, + preanimdelay: 0, + speed: UPDATE_INTERVAL, + } as ChatMsg; \ No newline at end of file diff --git a/webAO/viewport/constants/positions.ts b/webAO/viewport/constants/positions.ts new file mode 100644 index 0000000..1712ac6 --- /dev/null +++ b/webAO/viewport/constants/positions.ts @@ -0,0 +1,45 @@ +import { Positions } from '../interfaces/Positions' +import { Desk } from '../interfaces/Desk'; + +export const positions: Positions = { + def: { + bg: "defenseempty", + desk: { ao2: "defensedesk.png", ao1: "bancodefensa.png" } as Desk, + speedLines: "defense_speedlines.gif", + }, + pro: { + bg: "prosecutorempty", + desk: { ao2: "prosecutiondesk.png", ao1: "bancoacusacion.png" } as Desk, + speedLines: "prosecution_speedlines.gif", + }, + hld: { + bg: "helperstand", + desk: {} as Desk, + speedLines: "defense_speedlines.gif", + }, + hlp: { + bg: "prohelperstand", + desk: {} as Desk, + speedLines: "prosecution_speedlines.gif", + }, + wit: { + bg: "witnessempty", + desk: { ao2: "stand.png", ao1: "estrado.png" } as Desk, + speedLines: "prosecution_speedlines.gif", + }, + jud: { + bg: "judgestand", + desk: { ao2: "judgedesk.png", ao1: "judgedesk.gif" } as Desk, + speedLines: "prosecution_speedlines.gif", + }, + jur: { + bg: "jurystand", + desk: { ao2: "jurydesk.png", ao1: "estrado.png" } as Desk, + speedLines: "defense_speedlines.gif", + }, + sea: { + bg: "seancestand", + desk: { ao2: "seancedesk.png", ao1: "estrado.png" } as Desk, + speedLines: "prosecution_speedlines.gif", + }, +}; \ No newline at end of file diff --git a/webAO/viewport/constants/shouts.ts b/webAO/viewport/constants/shouts.ts new file mode 100644 index 0000000..eddd6d3 --- /dev/null +++ b/webAO/viewport/constants/shouts.ts @@ -0,0 +1 @@ +export const SHOUTS = [undefined, "holdit", "objection", "takethat", "custom"]; diff --git a/webAO/viewport/interfaces/ChatMsg.ts b/webAO/viewport/interfaces/ChatMsg.ts new file mode 100644 index 0000000..293a774 --- /dev/null +++ b/webAO/viewport/interfaces/ChatMsg.ts @@ -0,0 +1,34 @@ +export interface ChatMsg { + content: string; + objection: number; + sound: string; + startpreanim: boolean; + startspeaking: boolean; + side: any; + color: number; + snddelay: number; + preanimdelay: number; + speed: number; + blips: string; + self_offset?: number[]; + other_offset?: number[]; + showname?: string; + nameplate?: string; + flip?: number; + other_flip?: number; + effects?: string[]; + deskmod?: number; + preanim?: string; + other_name?: string; + sprite?: string; + name?: string; + chatbox?: string; + other_emote?: string; + parsed?: HTMLSpanElement[]; + screenshake?: number; + flash?: number; + type?: number; + evidence?: number; + looping_sfx?: boolean; + noninterrupting_preanim?: number; + } \ No newline at end of file diff --git a/webAO/viewport/interfaces/Desk.ts b/webAO/viewport/interfaces/Desk.ts new file mode 100644 index 0000000..872426a --- /dev/null +++ b/webAO/viewport/interfaces/Desk.ts @@ -0,0 +1,4 @@ +export interface Desk { + ao2?: string; + ao1?: string; +} \ No newline at end of file diff --git a/webAO/viewport/interfaces/Position.ts b/webAO/viewport/interfaces/Position.ts new file mode 100644 index 0000000..dea7238 --- /dev/null +++ b/webAO/viewport/interfaces/Position.ts @@ -0,0 +1,7 @@ +import { Desk } from './Desk' + +export interface Position { + bg?: string; + desk?: Desk; + speedLines: string; +} \ No newline at end of file diff --git a/webAO/viewport/interfaces/Positions.ts b/webAO/viewport/interfaces/Positions.ts new file mode 100644 index 0000000..0644962 --- /dev/null +++ b/webAO/viewport/interfaces/Positions.ts @@ -0,0 +1,5 @@ +import { Position } from './Position' + +export interface Positions { + [key: string]: Position; +} \ No newline at end of file diff --git a/webAO/viewport/interfaces/Testimony.ts b/webAO/viewport/interfaces/Testimony.ts new file mode 100644 index 0000000..61a7491 --- /dev/null +++ b/webAO/viewport/interfaces/Testimony.ts @@ -0,0 +1,3 @@ +export interface Testimony { + [key: number]: string; +} \ No newline at end of file diff --git a/webAO/viewport/interfaces/Viewport.ts b/webAO/viewport/interfaces/Viewport.ts new file mode 100644 index 0000000..3ffbcc8 --- /dev/null +++ b/webAO/viewport/interfaces/Viewport.ts @@ -0,0 +1,24 @@ +import { ChatMsg } from "./ChatMsg"; +export interface Viewport { + chat_tick: Function; + changeMusicVolume: Function; + reloadTheme: Function; + playSFX: Function; + set_side: Function; + initTestimonyUpdater: Function; + updateTestimony: Function; + disposeTestimony: Function; + handle_ic_speaking: Function; + handleTextTick: Function; + theme: string; + chatmsg: ChatMsg; + setSfxAudio: Function; + getSfxAudio: Function; + getBackgroundFolder: Function; + blipChannels: HTMLAudioElement[]; + music: any; + musicVolume: number; + setBackgroundName: Function; + lastChar: string; + getBackgroundName: Function; +} \ No newline at end of file diff --git a/webAO/viewport/utils/createBlipChannels.ts b/webAO/viewport/utils/createBlipChannels.ts new file mode 100644 index 0000000..6296b3b --- /dev/null +++ b/webAO/viewport/utils/createBlipChannels.ts @@ -0,0 +1,15 @@ +import { opusCheck } from "../../dom/opusCheck"; + +export const createBlipsChannels = () => { + const blipSelectors = document.getElementsByClassName( + "blipSound" + ) as HTMLCollectionOf; + + const blipChannels = [...blipSelectors]; + // Allocate multiple blip audio channels to make blips less jittery + blipChannels.forEach((channel: HTMLAudioElement) => (channel.volume = 0.5)); + blipChannels.forEach( + (channel: HTMLAudioElement) => (channel.onerror = opusCheck(channel)) + ); + return blipChannels; +}; \ No newline at end of file diff --git a/webAO/viewport/utils/createMusic.ts b/webAO/viewport/utils/createMusic.ts new file mode 100644 index 0000000..9bf5240 --- /dev/null +++ b/webAO/viewport/utils/createMusic.ts @@ -0,0 +1,13 @@ +import { opusCheck } from '../../dom/opusCheck' + +export const createMusic = () => { + const audioChannels = document.getElementsByClassName( + "audioChannel" + ) as HTMLCollectionOf; + let music = [...audioChannels]; + music.forEach((channel: HTMLAudioElement) => (channel.volume = 0.5)); + music.forEach( + (channel: HTMLAudioElement) => (channel.onerror = opusCheck(channel)) + ); + return music; +}; \ No newline at end of file diff --git a/webAO/viewport/utils/createSfxAudio.ts b/webAO/viewport/utils/createSfxAudio.ts new file mode 100644 index 0000000..7e03563 --- /dev/null +++ b/webAO/viewport/utils/createSfxAudio.ts @@ -0,0 +1,9 @@ +import { AO_HOST } from "../../client/aoHost"; + +export const createSfxAudio = () => { + const sfxAudio = document.getElementById( + "client_sfxaudio" + ) as HTMLAudioElement; + sfxAudio.src = `${AO_HOST}sounds/general/sfx-realization.opus`; + return sfxAudio; +}; \ No newline at end of file diff --git a/webAO/viewport/utils/createShoutAudio.ts b/webAO/viewport/utils/createShoutAudio.ts new file mode 100644 index 0000000..8211116 --- /dev/null +++ b/webAO/viewport/utils/createShoutAudio.ts @@ -0,0 +1,9 @@ +import { AO_HOST } from "../../client/aoHost"; + +export const createShoutAudio = () => { + const shoutAudio = document.getElementById( + "client_shoutaudio" + ) as HTMLAudioElement; + shoutAudio.src = `${AO_HOST}misc/default/objection.opus`; + return shoutAudio; +}; \ No newline at end of file diff --git a/webAO/viewport/utils/createTestimonyAudio.ts b/webAO/viewport/utils/createTestimonyAudio.ts new file mode 100644 index 0000000..2ff98f6 --- /dev/null +++ b/webAO/viewport/utils/createTestimonyAudio.ts @@ -0,0 +1,9 @@ +import { AO_HOST } from '../../client/aoHost' + +export const createTestimonyAudio = () => { + const testimonyAudio = document.getElementById( + "client_testimonyaudio" + ) as HTMLAudioElement; + testimonyAudio.src = `${AO_HOST}sounds/general/sfx-guilty.opus`; + return testimonyAudio; +}; \ No newline at end of file diff --git a/webAO/viewport/viewport.ts b/webAO/viewport/viewport.ts new file mode 100644 index 0000000..9772796 --- /dev/null +++ b/webAO/viewport/viewport.ts @@ -0,0 +1,901 @@ +import tryUrls from "../utils/tryUrls"; +import fileExists from "../utils/fileExists"; +import Client, { delay } from "../client"; +import { UPDATE_INTERVAL } from "../client"; +import { setChatbox } from "../dom/setChatbox"; +import { resizeChatbox } from "../dom/resizeChatbox"; +import transparentPng from "../constants/transparentPng"; +import mlConfig from "../utils/aoml"; +import setEmote from "../client/setEmote"; +import getAnimLength from "../utils/getAnimLength"; +import { safeTags } from "../encoding"; +import setCookie from "../utils/setCookie"; +import { AO_HOST } from "../client/aoHost"; +import { appendICLog } from "../client/appendICLog"; +import { checkCallword } from '../client/checkCallword' +import { Viewport } from './interfaces/Viewport' +import { createBlipsChannels } from './utils/createBlipChannels' +import { defaultChatMsg } from './constants/defaultChatMsg' +import { createMusic } from './utils/createMusic' +import { createSfxAudio } from './utils/createSfxAudio' +import { createShoutAudio } from './utils/createShoutAudio' +import { createTestimonyAudio } from './utils/createTestimonyAudio' +import { ChatMsg } from "./interfaces/ChatMsg"; +import { Testimony } from './interfaces/Testimony' +import { COLORS } from './constants/colors' +import { SHOUTS } from './constants/shouts' +import { positions } from './constants/positions' + +const viewport = (masterClient: Client): Viewport => { + let animating = false; + let attorneyMarkdown = mlConfig(AO_HOST); + let blipChannels = createBlipsChannels(); + let chatmsg = defaultChatMsg; + let client = masterClient; + let currentBlipChannel = 0; + let lastChar = ""; + let lastEvi = 0; + let music = createMusic(); + let musicVolume = 0; + let sfxAudio = createSfxAudio(); + let sfxplayed = 0; + let shoutTimer = 0; + let shoutaudio = createShoutAudio(); + let startFirstTickCheck: boolean; + let startSecondTickCheck: boolean; + let startThirdTickCheck: boolean; + let testimonyAudio = createTestimonyAudio(); + let testimonyTimer = 0; + let testimonyUpdater: any; + let textnow = ""; + let theme: string; + let tickTimer = 0; + let updater: any; + let backgroundName = ""; + const getSfxAudio = () => sfxAudio; + const setSfxAudio = (value: HTMLAudioElement) => { sfxAudio = value }; + const getBackgroundName = () => backgroundName; + const setBackgroundName = (value: string) => { backgroundName = value }; + const getBackgroundFolder = () => + `${AO_HOST}background/${encodeURI(backgroundName.toLowerCase())}/`; + + const playSFX = async (sfxname: string, looping: boolean) => { + sfxAudio.pause(); + sfxAudio.loop = looping; + sfxAudio.src = sfxname; + sfxAudio.play(); + }; + + /** + * Changes the viewport background based on a given position. + * + * Valid positions: `def, pro, hld, hlp, wit, jud, jur, sea` + * @param {string} position the position to change into + */ + const set_side = async ({ + position, + showSpeedLines, + showDesk, + }: { + position: string; + showSpeedLines: boolean; + showDesk: boolean; + }) => { + const view = document.getElementById("client_fullview"); + + let bench: HTMLImageElement; + if ("def,pro,wit".includes(position)) { + bench = ( + document.getElementById(`client_${position}_bench`) + ); + } else { + bench = document.getElementById("client_bench_classic"); + } + + let court: HTMLImageElement; + if ("def,pro,wit".includes(position)) { + court = ( + document.getElementById(`client_court_${position}`) + ); + } else { + court = document.getElementById("client_court_classic"); + } + + let bg; + let desk; + let speedLines; + + if ("def,pro,hld,hlp,wit,jud,jur,sea".includes(position)) { + bg = positions[position].bg; + desk = positions[position].desk; + speedLines = positions[position].speedLines; + } else { + bg = `${position}`; + desk = { ao2: `${position}_overlay.png`, ao1: "_overlay.png" }; + speedLines = "defense_speedlines.gif"; + } + + if (showSpeedLines === true) { + court.src = `${AO_HOST}themes/default/${encodeURI(speedLines)}`; + } else { + court.src = await tryUrls(getBackgroundFolder() + bg); + } + + if (showDesk === true && desk) { + const deskFilename = (await fileExists(getBackgroundFolder() + desk.ao2)) + ? desk.ao2 + : desk.ao1; + bench.src = getBackgroundFolder() + deskFilename; + bench.style.opacity = "1"; + } else { + bench.style.opacity = "0"; + } + + if ("def,pro,wit".includes(position)) { + view.style.display = ""; + document.getElementById("client_classicview").style.display = "none"; + switch (position) { + case "def": + view.style.left = "0"; + break; + case "wit": + view.style.left = "-200%"; + break; + case "pro": + view.style.left = "-400%"; + break; + } + } else { + view.style.display = "none"; + document.getElementById("client_classicview").style.display = ""; + } + }; + + /** + * Intialize testimony updater + */ + const initTestimonyUpdater = () => { + const testimonyFilenames: Testimony = { + 1: "witnesstestimony", + 2: "crossexamination", + 3: "notguilty", + 4: "guilty", + }; + + const testimony = testimonyFilenames[masterClient.testimonyID]; + if (!testimony) { + console.warn(`Invalid testimony ID ${masterClient.testimonyID}`); + return; + } + + testimonyAudio.src = masterClient.resources[testimony].sfx; + testimonyAudio.play(); + + const testimonyOverlay = ( + document.getElementById("client_testimony") + ); + testimonyOverlay.src = masterClient.resources[testimony].src; + testimonyOverlay.style.opacity = "1"; + + testimonyTimer = 0; + testimonyUpdater = setTimeout(() => updateTestimony(), UPDATE_INTERVAL); + }; + + /** + * Updates the testimony overaly + */ + const updateTestimony = () => { + const testimonyFilenames: Testimony = { + 1: "witnesstestimony", + 2: "crossexamination", + 3: "notguilty", + 4: "guilty", + }; + + // Update timer + testimonyTimer += UPDATE_INTERVAL; + + const testimony = testimonyFilenames[masterClient.testimonyID]; + const resource = masterClient.resources[testimony]; + if (!resource) { + disposeTestimony(); + return; + } + + if (testimonyTimer >= resource.duration) { + disposeTestimony(); + } else { + testimonyUpdater = setTimeout(() => updateTestimony(), UPDATE_INTERVAL); + } + }; + + /** + * Dispose the testimony overlay + */ + const disposeTestimony = () => { + masterClient.testimonyID = 0; + testimonyTimer = 0; + document.getElementById("client_testimony").style.opacity = "0"; + clearTimeout(testimonyUpdater); + }; + + /** + * Sets a new emote. + * 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 + */ + const handle_ic_speaking = async (playerChatMsg: ChatMsg) => { + chatmsg = playerChatMsg; + client.viewport.chatmsg = playerChatMsg; + + textnow = ""; + sfxplayed = 0; + tickTimer = 0; + animating = true; + startFirstTickCheck = true; + startSecondTickCheck = false; + startThirdTickCheck = false; + let charLayers = document.getElementById("client_char"); + let pairLayers = document.getElementById("client_pair_char"); + // stop updater + clearTimeout(updater); + + // stop last sfx from looping any longer + sfxAudio.loop = false; + + const fg = document.getElementById("client_fg"); + const gamewindow = document.getElementById("client_gamewindow"); + const waitingBox = document.getElementById("client_chatwaiting"); + + // Reset CSS animation + gamewindow.style.animation = ""; + waitingBox.style.opacity = "0"; + + const eviBox = document.getElementById("client_evi"); + + if (lastEvi !== chatmsg.evidence) { + eviBox.style.opacity = "0"; + eviBox.style.height = "0%"; + } + lastEvi = chatmsg.evidence; + + const validSides: string[] = ["def", "pro", "wit"]; // these are for the full view pan, the other positions use 'client_char' + if (validSides.includes(chatmsg.side)) { + charLayers = document.getElementById(`client_${chatmsg.side}_char`); + pairLayers = document.getElementById(`client_${chatmsg.side}_pair_char`); + } + + const chatContainerBox = document.getElementById("client_chatcontainer"); + const nameBoxInner = document.getElementById("client_inner_name"); + const chatBoxInner = document.getElementById("client_inner_chat"); + + const displayname = + (document.getElementById("showname")).checked && + chatmsg.showname !== "" + ? chatmsg.showname + : chatmsg.nameplate; + + // Clear out the last message + chatBoxInner.innerText = textnow; + nameBoxInner.innerText = displayname; + + if (lastChar !== chatmsg.name) { + charLayers.style.opacity = "0"; + pairLayers.style.opacity = "0"; + } + + lastChar = chatmsg.name; + client.viewport.lastChar = chatmsg.name; + + appendICLog(chatmsg.content, chatmsg.showname, chatmsg.nameplate); + + checkCallword(chatmsg.content, sfxAudio); + + setEmote( + AO_HOST, + client, + chatmsg.name.toLowerCase(), + chatmsg.sprite, + "(a)", + false, + chatmsg.side + ); + + if (chatmsg.other_name) { + setEmote( + AO_HOST, + client, + chatmsg.other_name.toLowerCase(), + chatmsg.other_emote, + "(a)", + false, + chatmsg.side + ); + } + + // gets which shout shall played + const shoutSprite = ( + document.getElementById("client_shout") + ); + const shout = SHOUTS[chatmsg.objection]; + if (shout) { + // Hide message box + chatContainerBox.style.opacity = "0"; + if (chatmsg.objection === 4) { + shoutSprite.src = `${AO_HOST}characters/${encodeURI( + chatmsg.name.toLowerCase() + )}/custom.gif`; + } else { + shoutSprite.src = masterClient.resources[shout].src; + shoutSprite.style.animation = "bubble 700ms steps(10, jump-both)"; + } + shoutSprite.style.opacity = "1"; + + shoutaudio.src = `${AO_HOST}characters/${encodeURI( + chatmsg.name.toLowerCase() + )}/${shout}.opus`; + shoutaudio.play(); + shoutTimer = masterClient.resources[shout].duration; + } else { + shoutTimer = 0; + } + + chatmsg.startpreanim = true; + let gifLength = 0; + + if (chatmsg.type === 1 && chatmsg.preanim !== "-") { + //we have a preanim + chatContainerBox.style.opacity = "0"; + gifLength = await getAnimLength( + `${AO_HOST}characters/${encodeURI( + chatmsg.name.toLowerCase() + )}/${encodeURI(chatmsg.preanim)}` + ); + console.debug("preanim is " + gifLength + " long"); + chatmsg.startspeaking = false; + } else { + chatmsg.startspeaking = true; + if (chatmsg.content !== "") chatContainerBox.style.opacity = "1"; + } + chatmsg.preanimdelay = gifLength; + const setAside = { + position: chatmsg.side, + showSpeedLines: false, + showDesk: false, + }; + let skipoffset: boolean = false; + if (chatmsg.type === 5) { + setAside.showSpeedLines = true; + setAside.showDesk = false; + set_side(setAside); + } else { + switch (Number(chatmsg.deskmod)) { + case 0: //desk is hidden + setAside.showSpeedLines = false; + setAside.showDesk = false; + set_side(setAside); + break; + case 1: //desk is shown + setAside.showSpeedLines = false; + setAside.showDesk = true; + set_side(setAside); + break; + case 2: //desk is hidden during preanim, but shown during idle/talk + setAside.showSpeedLines = false; + setAside.showDesk = false; + set_side(setAside); + break; + case 3: //opposite of 2 + setAside.showSpeedLines = false; + setAside.showDesk = false; + set_side(setAside); + break; + case 4: //desk is hidden, character offset is ignored, pair character is hidden during preanim, normal behavior during idle/talk + setAside.showSpeedLines = false; + setAside.showDesk = false; + set_side(setAside); + skipoffset = true; + break; + case 5: //opposite of 4 + setAside.showSpeedLines = false; + setAside.showDesk = true; + set_side(setAside); + break; + default: + setAside.showSpeedLines = false; + setAside.showDesk = true; + set_side(setAside); + break; + } + } + + setChatbox(chatmsg.chatbox); + resizeChatbox(); + + if (!skipoffset) { + // Flip the character + charLayers.style.transform = + chatmsg.flip === 1 ? "scaleX(-1)" : "scaleX(1)"; + pairLayers.style.transform = + chatmsg.other_flip === 1 ? "scaleX(-1)" : "scaleX(1)"; + + // Shift by the horizontal offset + switch (chatmsg.side) { + case "wit": + pairLayers.style.left = `${200 + Number(chatmsg.other_offset[0])}%`; + charLayers.style.left = `${200 + Number(chatmsg.self_offset[0])}%`; + break; + case "pro": + pairLayers.style.left = `${400 + Number(chatmsg.other_offset[0])}%`; + charLayers.style.left = `${400 + Number(chatmsg.self_offset[0])}%`; + break; + default: + pairLayers.style.left = `${Number(chatmsg.other_offset[0])}%`; + charLayers.style.left = `${Number(chatmsg.self_offset[0])}%`; + break; + } + + // New vertical offsets + pairLayers.style.top = `${Number(chatmsg.other_offset[1])}%`; + charLayers.style.top = `${Number(chatmsg.self_offset[1])}%`; + } + + blipChannels.forEach( + (channel: HTMLAudioElement) => + (channel.src = `${AO_HOST}sounds/general/sfx-blip${encodeURI( + chatmsg.blips.toLowerCase() + )}.opus`) + ); + + // process markup + if (chatmsg.content.startsWith("~~")) { + chatBoxInner.style.textAlign = "center"; + chatmsg.content = chatmsg.content.substring(2, chatmsg.content.length); + } else { + chatBoxInner.style.textAlign = "inherit"; + } + + // apply effects + fg.style.animation = ""; + const effectName = chatmsg.effects[0].toLowerCase(); + const badEffects = ["", "-", "none"]; + if (effectName.startsWith("rain")) { + (document.getElementById("effect_css")).href = "styles/effects/rain.css"; + let intensity = 200; + if (effectName.endsWith("weak")) { + intensity = 100; + } else if (effectName.endsWith("strong")) { + intensity = 400; + } + if (intensity < fg.childElementCount) + fg.innerHTML = ''; + else + intensity = intensity - fg.childElementCount; + + for (let i = 0; i < intensity; i++) { + let drop = document.createElement("p"); + drop.style.left = (Math.random() * 100) + "%"; + drop.style.animationDelay = String(Math.random()) + "s"; + fg.appendChild(drop) + } + } else if ( + chatmsg.effects[0] && + !badEffects.includes(effectName) + ) { + (document.getElementById("effect_css")).href = ""; + fg.innerHTML = ''; + const baseEffectUrl = `${AO_HOST}themes/default/effects/`; + fg.src = `${baseEffectUrl}${encodeURI(effectName)}.webp`; + } else { + fg.innerHTML = ''; + fg.src = transparentPng; + } + + charLayers.style.opacity = "1"; + + const soundChecks = ["0", "1", "", undefined]; + if (soundChecks.some((check) => chatmsg.sound === check)) { + chatmsg.sound = chatmsg.effects[2]; + } + chatmsg.parsed = await attorneyMarkdown.applyMarkdown( + chatmsg.content, + COLORS[chatmsg.color] + ); + chat_tick(); + }; + + const handleTextTick = async (charLayers: HTMLImageElement) => { + const chatBox = document.getElementById("client_chat"); + const waitingBox = document.getElementById("client_chatwaiting"); + const chatBoxInner = document.getElementById("client_inner_chat"); + const charName = chatmsg.name.toLowerCase(); + const charEmote = chatmsg.sprite.toLowerCase(); + + if (chatmsg.content.charAt(textnow.length) !== " ") { + blipChannels[currentBlipChannel].play(); + currentBlipChannel++; + currentBlipChannel %= blipChannels.length; + } + textnow = chatmsg.content.substring(0, textnow.length + 1); + const characterElement = chatmsg.parsed[textnow.length - 1]; + if (characterElement) { + const COMMAND_IDENTIFIER = "\\"; + + const nextCharacterElement = chatmsg.parsed[textnow.length]; + const flash = async () => { + const effectlayer = document.getElementById("client_fg"); + playSFX(`${AO_HOST}sounds/general/sfx-realization.opus`, false); + effectlayer.style.animation = "flash 0.4s 1"; + await delay(400); + effectlayer.style.removeProperty("animation"); + }; + + const shake = async () => { + const gamewindow = document.getElementById("client_gamewindow"); + playSFX(`${AO_HOST}sounds/general/sfx-stab.opus`, false); + gamewindow.style.animation = "shake 0.2s 1"; + await delay(200); + gamewindow.style.removeProperty("animation"); + }; + + const commands = new Map( + Object.entries({ + s: shake, + f: flash, + }) + ); + const textSpeeds = new Set(["{", "}"]); + + // Changing Text Speed + if (textSpeeds.has(characterElement.innerHTML)) { + // Grab them all in a row + const MAX_SLOW_CHATSPEED = 120; + for (let i = textnow.length; i < chatmsg.content.length; i++) { + const currentCharacter = chatmsg.parsed[i - 1].innerHTML; + if (currentCharacter === "}") { + if (chatmsg.speed > 0) { + chatmsg.speed -= 20; + } + } else if (currentCharacter === "{") { + if (chatmsg.speed < MAX_SLOW_CHATSPEED) { + chatmsg.speed += 20; + } + } else { + // No longer at a speed character + textnow = chatmsg.content.substring(0, i); + break; + } + } + } + + if ( + characterElement.innerHTML === COMMAND_IDENTIFIER && + commands.has(nextCharacterElement?.innerHTML) + ) { + textnow = chatmsg.content.substring(0, textnow.length + 1); + await commands.get(nextCharacterElement.innerHTML)(); + } else { + chatBoxInner.appendChild(chatmsg.parsed[textnow.length - 1]); + } + } + // scroll to bottom + chatBox.scrollTop = chatBox.scrollHeight; + + if (textnow === chatmsg.content) { + animating = false; + setEmote( + AO_HOST, + client, + charName, + charEmote, + "(a)", + false, + chatmsg.side + ); + charLayers.style.opacity = "1"; + waitingBox.style.opacity = "1"; + clearTimeout(updater); + } + }; + /** + * Updates the chatbox based on the given text. + * + * OK, here's the documentation on how this works: + * + * 1 _animating + * If we're not done with this characters animation, i.e. his text isn't fully there, set a timeout for the next tick/step to happen + * + * 2 startpreanim + * If the shout timer is over it starts with the preanim + * The first thing it checks for is the shake effect (TODO on client this is handled by the @ symbol and not a flag ) + * Then is the flash/realization effect + * After that, the shout image set to be transparent + * and the main characters preanim gif is loaded + * If pairing is supported the paired character will just stand around with his idle sprite + * + * 3 preanimdelay over + * this animates the evidence popup and finally shows the character name and message box + * it sets the text color and the character speaking sprite + * + * 4 textnow != content + * this adds a character to the textbox and stops the animations if the entire message is present in the textbox + * + * 5 sfx + * independent of the stuff above, this will play any sound effects specified by the emote the character sent. + * happens after the shout delay + an sfx delay that comes with the message packet + * + * XXX: This relies on a global variable `chatmsg`! + */ + const chat_tick = async () => { + // note: this is called fairly often + // do not perform heavy operations here + + await delay(chatmsg.speed); + if (textnow === chatmsg.content) { + return; + } + + const gamewindow = document.getElementById("client_gamewindow"); + const waitingBox = document.getElementById("client_chatwaiting"); + const eviBox = document.getElementById("client_evi"); + const shoutSprite = ( + document.getElementById("client_shout") + ); + const effectlayer = document.getElementById("client_fg"); + const chatBoxInner = document.getElementById("client_inner_chat"); + let charLayers = document.getElementById("client_char"); + let pairLayers = ( + document.getElementById("client_pair_char") + ); + + const validSides: string[] = ["def", "pro", "wit"]; // these are for the full view pan, the other positions use 'client_char' + if (validSides.includes(chatmsg.side)) { + charLayers = ( + document.getElementById(`client_${chatmsg.side}_char`) + ); + pairLayers = ( + document.getElementById(`client_${chatmsg.side}_pair_char`) + ); + } + + const charName = chatmsg.name.toLowerCase(); + const charEmote = chatmsg.sprite.toLowerCase(); + + const pairName = chatmsg.other_name.toLowerCase(); + const pairEmote = chatmsg.other_emote.toLowerCase(); + + // TODO: preanims sometimes play when they're not supposed to + const isShoutOver = tickTimer >= shoutTimer; + const isShoutAndPreanimOver = + tickTimer >= shoutTimer + chatmsg.preanimdelay; + if (isShoutOver && startFirstTickCheck) { + // Effect stuff + if (chatmsg.screenshake === 1) { + // Shake screen + playSFX(`${AO_HOST}sounds/general/sfx-stab.opus`, false); + gamewindow.style.animation = "shake 0.2s 1"; + } + if (chatmsg.flash === 1) { + // Flash screen + playSFX(`${AO_HOST}sounds/general/sfx-realization.opus`, false); + effectlayer.style.animation = "flash 0.4s 1"; + } + + // Pre-animation stuff + if (chatmsg.preanimdelay > 0) { + shoutSprite.style.opacity = "0"; + shoutSprite.style.animation = ""; + const preanim = chatmsg.preanim.toLowerCase(); + setEmote(AO_HOST, client, charName, preanim, "", false, chatmsg.side); + } + + if (chatmsg.other_name) { + pairLayers.style.opacity = "1"; + } else { + pairLayers.style.opacity = "0"; + } + // Done with first check, move to second + startFirstTickCheck = false; + startSecondTickCheck = true; + + chatmsg.startpreanim = false; + chatmsg.startspeaking = true; + } + + const hasNonInterruptingPreAnim = chatmsg.noninterrupting_preanim === 1; + if (textnow !== chatmsg.content && hasNonInterruptingPreAnim) { + const chatContainerBox = document.getElementById("client_chatcontainer"); + chatContainerBox.style.opacity = "1"; + await handleTextTick(charLayers); + } else if (isShoutAndPreanimOver && startSecondTickCheck) { + if (chatmsg.startspeaking) { + chatmsg.startspeaking = false; + + // Evidence Bullshit + if (chatmsg.evidence > 0) { + // Prepare evidence + eviBox.src = safeTags( + masterClient.evidences[chatmsg.evidence - 1].icon + ); + + eviBox.style.width = "auto"; + eviBox.style.height = "36.5%"; + eviBox.style.opacity = "1"; + + testimonyAudio.src = `${AO_HOST}sounds/general/sfx-evidenceshoop.opus`; + testimonyAudio.play(); + + if (chatmsg.side === "def") { + // Only def show evidence on right + eviBox.style.right = "1em"; + eviBox.style.left = "initial"; + } else { + eviBox.style.right = "initial"; + eviBox.style.left = "1em"; + } + } + chatBoxInner.className = `text_${COLORS[chatmsg.color]}`; + + if (chatmsg.preanimdelay === 0) { + shoutSprite.style.opacity = "0"; + shoutSprite.style.animation = ""; + } + + switch (Number(chatmsg.deskmod)) { + case 2: + set_side({ + position: chatmsg.side, + showSpeedLines: false, + showDesk: true, + }); + break; + case 3: + set_side({ + position: chatmsg.side, + showSpeedLines: false, + showDesk: false, + }); + break; + case 4: + set_side({ + position: chatmsg.side, + showSpeedLines: false, + showDesk: true, + }); + break; + case 5: + set_side({ + position: chatmsg.side, + showSpeedLines: false, + showDesk: false, + }); + break; + } + + if (chatmsg.other_name) { + 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 + ); + charLayers.style.opacity = "1"; + + if (textnow === chatmsg.content) { + setEmote( + AO_HOST, + client, + charName, + charEmote, + "(a)", + false, + chatmsg.side + ); + charLayers.style.opacity = "1"; + waitingBox.style.opacity = "1"; + animating = false; + clearTimeout(updater); + return; + } + } else if (textnow !== chatmsg.content) { + const chatContainerBox = document.getElementById( + "client_chatcontainer" + ); + chatContainerBox.style.opacity = "1"; + await handleTextTick(charLayers); + } + } + + if (!sfxplayed && chatmsg.snddelay + shoutTimer >= tickTimer) { + sfxplayed = 1; + if ( + chatmsg.sound !== "0" && + chatmsg.sound !== "1" && + chatmsg.sound !== "" && + 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 + ); + } + } + if (animating) { + chat_tick(); + } + tickTimer += UPDATE_INTERVAL; + }; + /** + * Triggered by the theme selector. + */ + function reloadTheme() { + theme = (document.getElementById("client_themeselect")) + .value; + + setCookie("theme", theme); + (( + document.getElementById("client_theme") + )).href = `styles/${theme}.css`; + } + window.reloadTheme = reloadTheme; + + const changeMusicVolume = (volume: number = -1) => { + const clientVolume = Number( + (document.getElementById("client_mvolume")).value + ); + let musicVolume = volume === -1 ? clientVolume : volume; + music.forEach( + (channel: HTMLAudioElement) => (channel.volume = musicVolume) + ); + setCookie("musicVolume", String(musicVolume)); + }; + window.changeMusicVolume = changeMusicVolume; + + return { + chat_tick, + changeMusicVolume, + reloadTheme, + playSFX, + set_side, + setBackgroundName, + initTestimonyUpdater, + updateTestimony, + disposeTestimony, + handle_ic_speaking, + handleTextTick, + getBackgroundFolder, + getBackgroundName, + getSfxAudio, + setSfxAudio, + theme, + chatmsg, + blipChannels, + lastChar, + music, + musicVolume, + }; +}; + +export default viewport; -- cgit