diff options
Diffstat (limited to 'webAO')
77 files changed, 1981 insertions, 1745 deletions
diff --git a/webAO/client.ts b/webAO/client.ts index e92489c..a86664c 100644 --- a/webAO/client.ts +++ b/webAO/client.ts @@ -3,35 +3,19 @@ * made by sD, refactored by oldmud0 and Qubrick * credits to aleks for original idea and source */ - +import { isLowMemory } from './client/isLowMemory' import FingerprintJS from "@fingerprintjs/fingerprintjs"; -import vanilla_background_arr from "./constants/backgrounds.js"; -import vanilla_evidence_arr from "./constants/evidence.js"; -import {sender, ISender} from './client/sender/index' -import iniParse from "./iniParse"; -import getCookie from "./utils/getCookie"; -import setCookie from "./utils/setCookie"; -import fileExists from "./utils/fileExists.js"; +import { sender, ISender } from './client/sender/index' import queryParser from "./utils/queryParser"; import getResources from "./utils/getResources.js"; -import downloadFile from "./services/downloadFile"; -import masterViewport, { Viewport } from "./viewport"; +import masterViewport from "./viewport/viewport"; +import { Viewport } from './viewport/interfaces/Viewport'; import { EventEmitter } from "events"; -import { area_click } from './dom/areaClick' import { onReplayGo } from './dom/onReplayGo' -import { escapeChat, safeTags, unescapeChat } from "./encoding"; -import { setChatbox } from "./dom/setChatbox"; -import { request } from "./services/request.js"; -import { - changeShoutVolume, - changeSFXVolume, - changeTestimonyVolume, -} from "./dom/changeVolume.js"; -import { getFilenameFromPath } from "./utils/paths"; import { packetHandler } from './packets/packetHandler' -import { showname_click } from './dom/showNameClick' +import { loadResources } from './client/loadResources' import { AO_HOST } from './client/aoHost' - +import { fetchBackgroundList, fetchEvidenceList, fetchCharacterList, fetchManifest } from './client/fetchLists' const version = process.env.npm_package_version; let { ip: serverIP, mode, theme } = queryParser(); @@ -77,38 +61,25 @@ export const setBanned = (val: boolean) => { } let hdid: string; -function isLowMemory() { - if ( - /webOS|iPod|BlackBerry|BB|PlayBook|IEMobile|Windows Phone|Kindle|Silk|PlayStation|Nintendo|Opera Mini/i.test( - navigator.userAgent - ) - ) { - oldLoading = true; - } -} - const fpPromise = FingerprintJS.load(); -const connect = (address: string) => { -} fpPromise .then((fp) => fp.get()) .then((result) => { hdid = result.visitorId; - console.log("NEW CLIENT"); - // Create the new client and connect it client = new Client(serverIP); client.connect() - isLowMemory(); - client.loadResources(); + loadResources(); }); export const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); -let lastICMessageTime = new Date(0); - +export let lastICMessageTime = new Date(0); +export const setLastICMessageTime = (val: Date) => { + lastICMessageTime = val +} class Client extends EventEmitter { serv: any; hp: number[]; @@ -132,8 +103,11 @@ class Client extends EventEmitter { sender: ISender; checkUpdater: any; _lastTimeICReceived: any; + manifest: string[]; viewport: Viewport; connect: () => void; + loadResources: () => void + isLowMemory: () => void constructor(address: string) { super(); @@ -168,13 +142,16 @@ class Client extends EventEmitter { this.musics = []; this.musics_time = false; this.callwords = []; + this.manifest = []; this.resources = getResources(AO_HOST, THEME); this.selectedEmote = -1; this.selectedEvidence = 0; this.checkUpdater = null; this.sender = sender - this.viewport = masterViewport(this); + this.viewport = masterViewport(); this._lastTimeICReceived = new Date(0); + loadResources + isLowMemory } /** @@ -200,8 +177,6 @@ class Client extends EventEmitter { : 0; } - - /** * Hook for sending messages to the client * @param {string} message the message to send @@ -216,87 +191,14 @@ class Client extends EventEmitter { * to the server. */ joinServer() { - console.log(this.sender) this.sender.sendServer(`HI#${hdid}#%`); - this.sender.sendServer("ID#webAO#webAO#%"); + this.sender.sendServer(`ID#webAO#${version}#%`); if (mode !== "replay") { this.checkUpdater = setInterval(() => this.sender.sendCheck(), 5000); } } /** - * Load game resources and stored settings. - */ - loadResources() { - document.getElementById("client_version").innerText = `version ${version}`; - - // Load background array to select - const background_select = <HTMLSelectElement>( - document.getElementById("bg_select") - ); - background_select.add(new Option("Custom", "0")); - vanilla_background_arr.forEach((background) => { - background_select.add(new Option(background)); - }); - - // Load evidence array to select - const evidence_select = <HTMLSelectElement>( - document.getElementById("evi_select") - ); - evidence_select.add(new Option("Custom", "0")); - vanilla_evidence_arr.forEach((evidence) => { - evidence_select.add(new Option(evidence)); - }); - - // Read cookies and set the UI to its values - (<HTMLInputElement>document.getElementById("OOC_name")).value = - getCookie("OOC_name") || - `web${String(Math.round(Math.random() * 100 + 10))}`; - - // Read cookies and set the UI to its values - const cookietheme = getCookie("theme") || "default"; - - (<HTMLOptionElement>( - document.querySelector(`#client_themeselect [value="${cookietheme}"]`) - )).selected = true; - this.viewport.reloadTheme(); - - const cookiechatbox = getCookie("chatbox") || "dynamic"; - - (<HTMLOptionElement>( - document.querySelector(`#client_chatboxselect [value="${cookiechatbox}"]`) - )).selected = true; - setChatbox(cookiechatbox); - - (<HTMLInputElement>document.getElementById("client_mvolume")).value = - getCookie("musicVolume") || "1"; - this.viewport.changeMusicVolume(); - (<HTMLAudioElement>document.getElementById("client_sfxaudio")).volume = - Number(getCookie("sfxVolume")) || 1; - changeSFXVolume(); - (<HTMLAudioElement>document.getElementById("client_shoutaudio")).volume = - Number(getCookie("shoutVolume")) || 1; - changeShoutVolume(); - (<HTMLAudioElement>( - document.getElementById("client_testimonyaudio") - )).volume = Number(getCookie("testimonyVolume")) || 1; - changeTestimonyVolume(); - (<HTMLInputElement>document.getElementById("client_bvolume")).value = - getCookie("blipVolume") || "1"; - this.viewport.changeBlipVolume(); - - (<HTMLInputElement>document.getElementById("ic_chat_name")).value = - getCookie("ic_chat_name"); - (<HTMLInputElement>document.getElementById("showname")).checked = Boolean( - getCookie("showname") - ); - showname_click(null); - - (<HTMLInputElement>document.getElementById("client_callwords")).value = - getCookie("callwords"); - } - - /** * Triggered when a connection is established to the server. */ onOpen(_e: Event) { @@ -335,7 +237,6 @@ class Client extends EventEmitter { packetHandler.has(packetHeader) ? packetHandler.get(packetHeader)(splitPacket) : console.warn(`Invalid packet header ${packetHeader}`); - } /** @@ -353,7 +254,6 @@ class Client extends EventEmitter { */ cleanup() { clearInterval(this.checkUpdater); - this.serv.close(); } @@ -383,129 +283,6 @@ class Client extends EventEmitter { } } - saveChatlogHandle = async () => { - const clientLog = document.getElementById("client_log"); - const icMessageLogs = clientLog.getElementsByTagName("p"); - const messages = []; - - for (let i = 0; i < icMessageLogs.length; i++) { - const SHOWNAME_POSITION = 0; - const TEXT_POSITION = 2; - const showname = icMessageLogs[i].children[SHOWNAME_POSITION].innerHTML; - const text = icMessageLogs[i].children[TEXT_POSITION].innerHTML; - const message = `${showname}: ${text}`; - messages.push(message); - } - const d = new Date(); - let ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(d); - let mo = new Intl.DateTimeFormat("en", { month: "short" }).format(d); - let da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(d); - - const filename = `chatlog-${da}-${mo}-${ye}`.toLowerCase(); - downloadFile(messages.join("\n"), filename); - - // Reset Chatbox to Empty - (<HTMLInputElement>document.getElementById("client_inputbox")).value = ""; - }; - - /** - * Handles the incoming character information, and downloads the sprite + ini for it - * @param {Array} chargs packet arguments - * @param {Number} charid character ID - */ - async handleCharacterInfo(chargs: string[], charid: number) { - const img = <HTMLImageElement>document.getElementById(`demo_${charid}`); - if (chargs[0]) { - let cini: any = {}; - const getCharIcon = async () => { - const extensions = [".png", ".webp"]; - img.alt = chargs[0]; - const charIconBaseUrl = `${AO_HOST}characters/${encodeURI( - chargs[0].toLowerCase() - )}/char_icon`; - for (let i = 0; i < extensions.length; i++) { - const fileUrl = charIconBaseUrl + extensions[i]; - const exists = await fileExists(fileUrl); - if (exists) { - img.alt = chargs[0]; - img.title = chargs[0]; - img.src = fileUrl; - return; - } - } - }; - getCharIcon(); - - // If the ini doesn't exist on the server this will throw an error - try { - const cinidata = await request( - `${AO_HOST}characters/${encodeURI(chargs[0].toLowerCase())}/char.ini` - ); - cini = iniParse(cinidata); - } catch (err) { - cini = {}; - img.classList.add("noini"); - console.warn(`character ${chargs[0]} is missing from webAO`); - // If it does, give the user a visual indication that the character is unusable - } - - const mute_select = <HTMLSelectElement>( - document.getElementById("mute_select") - ); - mute_select.add(new Option(safeTags(chargs[0]), String(charid))); - const pair_select = <HTMLSelectElement>( - document.getElementById("pair_select") - ); - pair_select.add(new Option(safeTags(chargs[0]), String(charid))); - - // sometimes ini files lack important settings - const default_options = { - name: chargs[0], - showname: chargs[0], - side: "def", - blips: "male", - chat: "", - category: "", - }; - cini.options = Object.assign(default_options, cini.options); - - // sometimes ini files lack important settings - const default_emotions = { - number: 0, - }; - cini.emotions = Object.assign(default_emotions, cini.emotions); - - this.chars[charid] = { - name: safeTags(chargs[0]), - showname: safeTags(cini.options.showname), - desc: safeTags(chargs[1]), - blips: safeTags(cini.options.blips).toLowerCase(), - gender: safeTags(cini.options.gender).toLowerCase(), - side: safeTags(cini.options.side).toLowerCase(), - chat: - cini.options.chat === "" - ? safeTags(cini.options.category).toLowerCase() - : safeTags(cini.options.chat).toLowerCase(), - evidence: chargs[3], - icon: img.src, - inifile: cini, - muted: false, - }; - - if ( - this.chars[charid].blips === "male" && - this.chars[charid].gender !== "male" && - this.chars[charid].gender !== "" - ) { - this.chars[charid].blips = this.chars[charid].gender; - } - - } else { - console.warn(`missing charid ${charid}`); - img.style.display = "none"; - } - } - resetMusicList() { this.musics = []; document.getElementById("client_musiclist").innerHTML = ""; @@ -515,231 +292,11 @@ class Client extends EventEmitter { this.areas = []; document.getElementById("areas").innerHTML = ""; - this.fetchBackgroundList(); - this.fetchEvidenceList(); - } - - async fetchBackgroundList() { - try { - const bgdata = await request(`${AO_HOST}backgrounds.json`); - const bg_array = JSON.parse(bgdata); - // the try catch will fail before here when there is no file - - const bg_select = <HTMLSelectElement>document.getElementById("bg_select"); - bg_select.innerHTML = ""; - - bg_select.add(new Option("Custom", "0")); - bg_array.forEach((background: string) => { - bg_select.add(new Option(background)); - }); - } catch (err) { - console.warn("there was no backgrounds.json file"); - } - } - - async fetchCharacterList() { - try { - const chardata = await request(`${AO_HOST}characters.json`); - const char_array = JSON.parse(chardata); - // the try catch will fail before here when there is no file - - const char_select = <HTMLSelectElement>( - document.getElementById("client_ininame") - ); - char_select.innerHTML = ""; - - char_array.forEach((character: string) => { - char_select.add(new Option(character)); - }); - } catch (err) { - console.warn("there was no characters.json file"); - } - } - - async fetchEvidenceList() { - try { - const evidata = await request(`${AO_HOST}evidence.json`); - const evi_array = JSON.parse(evidata); - // the try catch will fail before here when there is no file - - const evi_select = <HTMLSelectElement>( - document.getElementById("evi_select") - ); - evi_select.innerHTML = ""; - - evi_array.forEach((evi: string) => { - evi_select.add(new Option(evi)); - }); - evi_select.add(new Option("Custom", "0")); - } catch (err) { - console.warn("there was no evidence.json file"); - } - } - - isAudio(trackname: string) { - const audioEndings = [".wav", ".mp3", ".ogg", ".opus"]; - return ( - audioEndings.filter((ending) => trackname.endsWith(ending)).length === 1 - ); + fetchBackgroundList(); + fetchEvidenceList(); + fetchCharacterList(); } - addTrack(trackname: string) { - const newentry = <HTMLOptionElement>document.createElement("OPTION"); - const songName = getFilenameFromPath(trackname); - newentry.text = unescapeChat(songName); - newentry.value = trackname; - (<HTMLSelectElement>( - document.getElementById("client_musiclist") - )).options.add(newentry); - this.musics.push(trackname); - } - - createArea(id: number, name: string) { - const thisarea = { - name, - players: 0, - status: "IDLE", - cm: "", - locked: "FREE", - }; - - this.areas.push(thisarea); - - // Create area button - const newarea = document.createElement("SPAN"); - newarea.className = "area-button area-default"; - newarea.id = `area${id}`; - newarea.innerText = thisarea.name; - newarea.title = - `Players: ${thisarea.players}\n` + - `Status: ${thisarea.status}\n` + - `CM: ${thisarea.cm}\n` + - `Area lock: ${thisarea.locked}`; - newarea.onclick = function () { - area_click(newarea); - }; - - document.getElementById("areas").appendChild(newarea); - } - - /** - * Area list fuckery - */ - fix_last_area() { - if (this.areas.length > 0) { - const malplaced = this.areas.pop().name; - const areas = document.getElementById("areas"); - areas.removeChild(areas.lastChild); - this.addTrack(malplaced); - } - } - - /** - * Handles the kicked packet - * @param {string} type is it a kick or a ban - * @param {string} reason why - */ - handleBans(type: string, reason: string) { - document.getElementById("client_error").style.display = "flex"; - document.getElementById( - "client_errortext" - ).innerHTML = `${type}:<br>${reason.replace(/\n/g, "<br />")}`; - (<HTMLElement>( - document.getElementsByClassName("client_reconnect")[0] - )).style.display = "none"; - (<HTMLElement>( - document.getElementsByClassName("client_reconnect")[1] - )).style.display = "none"; - } -} - -/** - * Resets the IC parameters for the player to enter a new chat message. - * This should only be called when the player's previous chat message - * was successfully sent/presented. - */ -export function resetICParams() { - (<HTMLInputElement>document.getElementById("client_inputbox")).value = ""; - document.getElementById("button_flash").className = "client_button"; - document.getElementById("button_shake").className = "client_button"; - - (<HTMLInputElement>document.getElementById("sendpreanim")).checked = false; - (<HTMLInputElement>document.getElementById("sendsfx")).checked = false; - - if (selectedShout) { - document.getElementById(`button_${selectedShout}`).className = - "client_button"; - selectedShout = 0; - } -} - -/** - * Appends a message to the in-character chat log. - * @param {string} msg the string to be added - * @param {string} name the name of the sender - */ -export function appendICLog( - msg: string, - showname = "", - nameplate = "", - time = new Date() -) { - const entry = document.createElement("p"); - const shownameField = document.createElement("span"); - const nameplateField = document.createElement("span"); - const textField = document.createElement("span"); - nameplateField.className = "iclog_name iclog_nameplate"; - nameplateField.appendChild(document.createTextNode(nameplate)); - - shownameField.className = "iclog_name iclog_showname"; - if (showname === "" || !showname) { - shownameField.appendChild(document.createTextNode(nameplate)); - } else { - shownameField.appendChild(document.createTextNode(showname)); - } - - textField.className = "iclog_text"; - textField.appendChild(document.createTextNode(msg)); - - entry.appendChild(shownameField); - entry.appendChild(nameplateField); - entry.appendChild(textField); - - // Only put a timestamp if the minute has changed. - if (lastICMessageTime.getMinutes() !== time.getMinutes()) { - const timeStamp = document.createElement("span"); - timeStamp.className = "iclog_time"; - timeStamp.innerText = time.toLocaleTimeString(undefined, { - hour: "numeric", - minute: "2-digit", - }); - entry.appendChild(timeStamp); - } - - const clientLog = document.getElementById("client_log"); - clientLog.appendChild(entry); - - /* This is a little buggy - some troubleshooting might be desirable */ - if (clientLog.scrollTop > clientLog.scrollHeight - 800) { - clientLog.scrollTop = clientLog.scrollHeight; - } - - lastICMessageTime = new Date(); -} - -/** - * check if the message contains an entry on our callword list - * @param {string} message - */ -export function checkCallword(message: string, sfxAudio: HTMLAudioElement) { - client.callwords.forEach(testCallword); - function testCallword(item: string) { - if (item !== "" && message.toLowerCase().includes(item.toLowerCase())) { - sfxAudio.pause(); - sfxAudio.src = `${AO_HOST}sounds/general/sfx-gallery.opus`; - sfxAudio.play(); - } - } } -export default Client;
\ No newline at end of file +export default Client; diff --git a/webAO/client/addTrack.ts b/webAO/client/addTrack.ts new file mode 100644 index 0000000..247f07e --- /dev/null +++ b/webAO/client/addTrack.ts @@ -0,0 +1,15 @@ +import { client } from "../client"; +import { unescapeChat } from "../encoding"; +import { getFilenameFromPath } from "../utils/paths"; + + +export const addTrack = (trackname: string) => { + const newentry = <HTMLOptionElement>document.createElement("OPTION"); + const songName = getFilenameFromPath(trackname); + newentry.text = unescapeChat(songName); + newentry.value = trackname; + (<HTMLSelectElement>( + document.getElementById("client_musiclist") + )).options.add(newentry); + client.musics.push(trackname); +}
\ No newline at end of file diff --git a/webAO/client/appendICLog.ts b/webAO/client/appendICLog.ts new file mode 100644 index 0000000..f8b7852 --- /dev/null +++ b/webAO/client/appendICLog.ts @@ -0,0 +1,57 @@ +import { lastICMessageTime, setLastICMessageTime } from "../client"; + + + +/** + * Appends a message to the in-character chat log. + * @param {string} msg the string to be added + * @param {string} name the name of the sender + */ +export function appendICLog( + msg: string, + showname = "", + nameplate = "", + time = new Date() +) { + const entry = document.createElement("p"); + const shownameField = document.createElement("span"); + const nameplateField = document.createElement("span"); + const textField = document.createElement("span"); + nameplateField.className = "iclog_name iclog_nameplate"; + nameplateField.appendChild(document.createTextNode(nameplate)); + + shownameField.className = "iclog_name iclog_showname"; + if (showname === "" || !showname) { + shownameField.appendChild(document.createTextNode(nameplate)); + } else { + shownameField.appendChild(document.createTextNode(showname)); + } + + textField.className = "iclog_text"; + textField.appendChild(document.createTextNode(msg)); + + entry.appendChild(shownameField); + entry.appendChild(nameplateField); + entry.appendChild(textField); + + // Only put a timestamp if the minute has changed. + if (lastICMessageTime.getMinutes() !== time.getMinutes()) { + const timeStamp = document.createElement("span"); + timeStamp.className = "iclog_time"; + timeStamp.innerText = time.toLocaleTimeString(undefined, { + hour: "numeric", + minute: "2-digit", + }); + entry.appendChild(timeStamp); + } + + const clientLog = document.getElementById("client_log")!; + clientLog.appendChild(entry); + + /* This is a little buggy - some troubleshooting might be desirable */ + if (clientLog.scrollTop > clientLog.scrollHeight - 800) { + clientLog.scrollTop = clientLog.scrollHeight; + } + + setLastICMessageTime(new Date()); +}
\ No newline at end of file diff --git a/webAO/client/checkCallword.ts b/webAO/client/checkCallword.ts new file mode 100644 index 0000000..f6cffbc --- /dev/null +++ b/webAO/client/checkCallword.ts @@ -0,0 +1,17 @@ +import { client } from "../client"; +import { AO_HOST } from "./aoHost"; + +/** + * check if the message contains an entry on our callword list + * @param {string} message + */ +export function checkCallword(message: string, sfxAudio: HTMLAudioElement) { + client.callwords.forEach(testCallword); + function testCallword(item: string) { + if (item !== "" && message.toLowerCase().includes(item.toLowerCase())) { + sfxAudio.pause(); + sfxAudio.src = `${AO_HOST}sounds/general/sfx-gallery.opus`; + sfxAudio.play(); + } + } +}
\ No newline at end of file diff --git a/webAO/client/createArea.ts b/webAO/client/createArea.ts new file mode 100644 index 0000000..63af644 --- /dev/null +++ b/webAO/client/createArea.ts @@ -0,0 +1,30 @@ +import { client } from "../client"; +import { area_click } from "../dom/areaClick"; + +export const createArea = (id: number, name: string) => { + const thisarea = { + name, + players: 0, + status: "IDLE", + cm: "", + locked: "FREE", + }; + + client.areas.push(thisarea); + + // Create area button + const newarea = document.createElement("SPAN"); + newarea.className = "area-button area-default"; + newarea.id = `area${id}`; + newarea.innerText = thisarea.name; + newarea.title = + `Players: ${thisarea.players}\n` + + `Status: ${thisarea.status}\n` + + `CM: ${thisarea.cm}\n` + + `Area lock: ${thisarea.locked}`; + newarea.onclick = function () { + area_click(newarea); + }; + + document.getElementById("areas")!.appendChild(newarea); +}
\ No newline at end of file diff --git a/webAO/client/fetchLists.ts b/webAO/client/fetchLists.ts new file mode 100644 index 0000000..9efd181 --- /dev/null +++ b/webAO/client/fetchLists.ts @@ -0,0 +1,77 @@ +import { client } from "../client"; +import { AO_HOST } from "./aoHost"; +import { request } from "../services/request.js"; + +export const fetchBackgroundList = async () => { + try { + const bgdata = await request(`${AO_HOST}backgrounds.json`); + const bg_array = JSON.parse(bgdata); + // the try catch will fail before here when there is no file + + const bg_select = <HTMLSelectElement>document.getElementById("bg_select"); + bg_select.innerHTML = ""; + + bg_select.add(new Option("Custom", "0")); + bg_array.forEach((background: string) => { + bg_select.add(new Option(background)); + }); + } catch (err) { + console.warn("there was no backgrounds.json file"); + } +} + +export const fetchCharacterList = async () => { + const char_select = <HTMLSelectElement>( + document.getElementById("client_iniselect") + ); + char_select.innerHTML = ""; + + char_select.add(new Option("Custom", "0")); + + try { + const chardata = await request(`${AO_HOST}characters.json`); + const char_array = JSON.parse(chardata); + // the try catch will fail before here when there is no file + + char_array.forEach((character: string) => { + char_select.add(new Option(character)); + }); + } catch (err) { + console.warn("there was no characters.json file"); + } +} + + +export const fetchEvidenceList = async () => { + const evi_select = <HTMLSelectElement>( + document.getElementById("evi_select") + ); + evi_select.innerHTML = ""; + + evi_select.add(new Option("Custom", "0")); + + try { + const evidata = await request(`${AO_HOST}evidence.json`); + const evi_array = JSON.parse(evidata); + // the try catch will fail before here when there is no file + + evi_array.forEach((evi: string) => { + evi_select.add(new Option(evi)); + }); + + } catch (err) { + console.warn("there was no evidence.json file"); + } +} + + +export const fetchManifest = async () => { + try { + const manifestdata = await request(`${AO_HOST}manifest.txt`); + client.manifest = manifestdata.split(/\r\n|\n\r|\n|\r/); + // the try catch will fail before here when there is no file + + } catch (err) { + console.warn("there was no manifest.txt file"); + } +}
\ No newline at end of file diff --git a/webAO/client/fixLastArea.ts b/webAO/client/fixLastArea.ts new file mode 100644 index 0000000..f1aa99f --- /dev/null +++ b/webAO/client/fixLastArea.ts @@ -0,0 +1,15 @@ +import { client } from "../client"; +import { addTrack } from "./addTrack"; + + +/** + * Area list fuckery + */ +export const fix_last_area = () => { + if (client.areas.length > 0) { + const malplaced = client.areas.pop().name; + const areas = document.getElementById("areas")!; + areas.removeChild(areas.lastChild); + addTrack(malplaced); + } +}
\ No newline at end of file diff --git a/webAO/client/handleBans.ts b/webAO/client/handleBans.ts new file mode 100644 index 0000000..b977fc8 --- /dev/null +++ b/webAO/client/handleBans.ts @@ -0,0 +1,17 @@ +/** + * Handles the kicked packet + * @param {string} type is it a kick or a ban + * @param {string} reason why + */ +export const handleBans = (type: string, reason: string) => { + document.getElementById("client_error")!.style.display = "flex"; + document.getElementById( + "client_errortext" + )!.innerHTML = `${type}:<br>${reason.replace(/\n/g, "<br />")}`; + (<HTMLElement>( + document.getElementsByClassName("client_reconnect")[0] + )).style.display = "none"; + (<HTMLElement>( + document.getElementsByClassName("client_reconnect")[1] + )).style.display = "none"; +}
\ No newline at end of file diff --git a/webAO/client/handleCharacterInfo.ts b/webAO/client/handleCharacterInfo.ts new file mode 100644 index 0000000..9d74a8b --- /dev/null +++ b/webAO/client/handleCharacterInfo.ts @@ -0,0 +1,105 @@ +import { client } from "../client"; +import { safeTags } from "../encoding"; +import iniParse from "../iniParse"; +import request from "../services/request"; +import fileExists from "../utils/fileExists"; +import { AO_HOST } from "./aoHost"; + + +/** + * Handles the incoming character information, and downloads the sprite + ini for it + * @param {Array} chargs packet arguments + * @param {Number} charid character ID + */ +export const handleCharacterInfo = async (chargs: string[], charid: number) => { + const img = <HTMLImageElement>document.getElementById(`demo_${charid}`); + if (chargs[0]) { + let cini: any = {}; + const getCharIcon = async () => { + const extensions = [".png", ".webp"]; + img.alt = chargs[0]; + const charIconBaseUrl = `${AO_HOST}characters/${encodeURI( + chargs[0].toLowerCase() + )}/char_icon`; + for (let i = 0; i < extensions.length; i++) { + const fileUrl = charIconBaseUrl + extensions[i]; + const exists = await fileExists(fileUrl); + if (exists) { + img.alt = chargs[0]; + img.title = chargs[0]; + img.src = fileUrl; + return; + } + } + }; + getCharIcon(); + + // If the ini doesn't exist on the server this will throw an error + try { + const cinidata = await request( + `${AO_HOST}characters/${encodeURI(chargs[0].toLowerCase())}/char.ini` + ); + cini = iniParse(cinidata); + } catch (err) { + cini = {}; + img.classList.add("noini"); + console.warn(`character ${chargs[0]} is missing from webAO`); + // If it does, give the user a visual indication that the character is unusable + } + + const mute_select = <HTMLSelectElement>( + document.getElementById("mute_select") + ); + mute_select.add(new Option(safeTags(chargs[0]), String(charid))); + const pair_select = <HTMLSelectElement>( + document.getElementById("pair_select") + ); + pair_select.add(new Option(safeTags(chargs[0]), String(charid))); + + // sometimes ini files lack important settings + const default_options = { + name: chargs[0], + showname: chargs[0], + side: "def", + blips: "male", + chat: "", + category: "", + }; + cini.options = Object.assign(default_options, cini.options); + + // sometimes ini files lack important settings + const default_emotions = { + number: 0, + }; + cini.emotions = Object.assign(default_emotions, cini.emotions); + + client.chars[charid] = { + name: safeTags(chargs[0]), + showname: safeTags(cini.options.showname), + desc: safeTags(chargs[1]), + blips: safeTags(cini.options.blips).toLowerCase(), + gender: safeTags(cini.options.gender).toLowerCase(), + side: safeTags(cini.options.side).toLowerCase(), + chat: + cini.options.chat === "" + ? safeTags(cini.options.category).toLowerCase() + : safeTags(cini.options.chat).toLowerCase(), + evidence: chargs[3], + icon: img.src, + inifile: cini, + muted: false, + }; + + if ( + client.chars[charid].blips === "male" && + client.chars[charid].gender !== "male" && + client.chars[charid].gender !== "" + ) { + client.chars[charid].blips = client.chars[charid].gender; + } + + } else { + console.warn(`missing charid ${charid}`); + img.style.display = "none"; + } +}
\ No newline at end of file diff --git a/webAO/client/isAudio.ts b/webAO/client/isAudio.ts new file mode 100644 index 0000000..430f543 --- /dev/null +++ b/webAO/client/isAudio.ts @@ -0,0 +1,6 @@ +export const isAudio = (trackname: string) => { + const audioEndings = [".wav", ".mp3", ".ogg", ".opus"]; + return ( + audioEndings.filter((ending) => trackname.endsWith(ending)).length === 1 + ); +}
\ No newline at end of file diff --git a/webAO/client/isLowMemory.ts b/webAO/client/isLowMemory.ts new file mode 100644 index 0000000..caa6784 --- /dev/null +++ b/webAO/client/isLowMemory.ts @@ -0,0 +1,10 @@ +import { setOldLoading } from '../client' +export const isLowMemory = () => { + if ( + /webOS|iPod|BlackBerry|BB|PlayBook|IEMobile|Windows Phone|Kindle|Silk|PlayStation|Nintendo|Opera Mini/i.test( + navigator.userAgent + ) + ) { + setOldLoading(true); + } +} diff --git a/webAO/client/loadResources.ts b/webAO/client/loadResources.ts new file mode 100644 index 0000000..4954966 --- /dev/null +++ b/webAO/client/loadResources.ts @@ -0,0 +1,81 @@ +import getCookie from "../utils/getCookie"; +import vanilla_evidence_arr from "../constants/evidence.js"; +import vanilla_background_arr from "../constants/backgrounds.js"; +import { changeMusicVolume } from '../dom/changeMusicVolume' +import { setChatbox } from "../dom/setChatbox"; +import { changeSFXVolume, changeShoutVolume, changeTestimonyVolume } from "../dom/changeVolume"; +import { showname_click } from "../dom/showNameClick"; +import { changeBlipVolume } from '../dom/changeBlipVolume' +import { reloadTheme } from '../dom/reloadTheme' +const version = process.env.npm_package_version; + +/** + * Load game resources and stored settings. + */ +export const loadResources = () => { + document.getElementById("client_version")!.innerText = `version ${version}`; + // Load background array to select + const background_select = <HTMLSelectElement>( + document.getElementById("bg_select") + ); + background_select.add(new Option("Custom", "0")); + vanilla_background_arr.forEach((background) => { + background_select.add(new Option(background)); + }); + + // Load evidence array to select + const evidence_select = <HTMLSelectElement>( + document.getElementById("evi_select") + ); + evidence_select.add(new Option("Custom", "0")); + vanilla_evidence_arr.forEach((evidence) => { + evidence_select.add(new Option(evidence)); + }); + + // Read cookies and set the UI to its values + (<HTMLInputElement>document.getElementById("OOC_name")).value = + getCookie("OOC_name") || + `web${String(Math.round(Math.random() * 100 + 10))}`; + + // Read cookies and set the UI to its values + const cookietheme = getCookie("theme") || "default"; + + (<HTMLOptionElement>( + document.querySelector(`#client_themeselect [value="${cookietheme}"]`) + )).selected = true; + reloadTheme(); + + const cookiechatbox = getCookie("chatbox") || "dynamic"; + + (<HTMLOptionElement>( + document.querySelector(`#client_chatboxselect [value="${cookiechatbox}"]`) + )).selected = true; + setChatbox(cookiechatbox); + + (<HTMLInputElement>document.getElementById("client_mvolume")).value = + getCookie("musicVolume") || "1"; + changeMusicVolume(); + (<HTMLAudioElement>document.getElementById("client_sfxaudio")).volume = + Number(getCookie("sfxVolume")) || 1; + changeSFXVolume(); + (<HTMLAudioElement>document.getElementById("client_shoutaudio")).volume = + Number(getCookie("shoutVolume")) || 1; + changeShoutVolume(); + (<HTMLAudioElement>( + document.getElementById("client_testimonyaudio") + )).volume = Number(getCookie("testimonyVolume")) || 1; + changeTestimonyVolume(); + (<HTMLInputElement>document.getElementById("client_bvolume")).value = + getCookie("blipVolume") || "1"; + changeBlipVolume(); + + (<HTMLInputElement>document.getElementById("ic_chat_name")).value = + getCookie("ic_chat_name"); + (<HTMLInputElement>document.getElementById("showname")).checked = Boolean( + getCookie("showname") + ); + showname_click(null); + + (<HTMLInputElement>document.getElementById("client_callwords")).value = + getCookie("callwords"); +}
\ No newline at end of file diff --git a/webAO/client/resetICParams.ts b/webAO/client/resetICParams.ts new file mode 100644 index 0000000..414da27 --- /dev/null +++ b/webAO/client/resetICParams.ts @@ -0,0 +1,21 @@ +import { selectedShout, setSelectedShout } from "../client"; + +/** + * Resets the IC parameters for the player to enter a new chat message. + * This should only be called when the player's previous chat message + * was successfully sent/presented. + */ +export function resetICParams() { + (<HTMLInputElement>document.getElementById("client_inputbox")).value = ""; + document.getElementById("button_flash")!.className = "client_button"; + document.getElementById("button_shake")!.className = "client_button"; + + (<HTMLInputElement>document.getElementById("sendpreanim")).checked = false; + (<HTMLInputElement>document.getElementById("sendsfx")).checked = false; + + if (selectedShout) { + document.getElementById(`button_${selectedShout}`)!.className = + "client_button"; + setSelectedShout(0); + } +}
\ No newline at end of file diff --git a/webAO/client/saveChatLogHandle.ts b/webAO/client/saveChatLogHandle.ts new file mode 100644 index 0000000..bcc1075 --- /dev/null +++ b/webAO/client/saveChatLogHandle.ts @@ -0,0 +1,26 @@ +import downloadFile from "../services/downloadFile"; + +export const saveChatlogHandle = async () => { + const clientLog = document.getElementById("client_log")!; + const icMessageLogs = clientLog.getElementsByTagName("p"); + const messages: string[] = []; + + for (let i = 0; i < icMessageLogs.length; i++) { + const SHOWNAME_POSITION = 0; + const TEXT_POSITION = 2; + const showname = icMessageLogs[i].children[SHOWNAME_POSITION].innerHTML; + const text = icMessageLogs[i].children[TEXT_POSITION].innerHTML; + const message = `${showname}: ${text}`; + messages.push(message); + } + const d = new Date(); + let ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(d); + let mo = new Intl.DateTimeFormat("en", { month: "short" }).format(d); + let da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(d); + + const filename = `chatlog-${da}-${mo}-${ye}`.toLowerCase(); + downloadFile(messages.join("\n"), filename); + + // Reset Chatbox to Empty + (<HTMLInputElement>document.getElementById("client_inputbox")).value = ""; +};
\ No newline at end of file diff --git a/webAO/client/sender/sendOOC.ts b/webAO/client/sender/sendOOC.ts index a410b5f..9674ad9 100644 --- a/webAO/client/sender/sendOOC.ts +++ b/webAO/client/sender/sendOOC.ts @@ -1,7 +1,7 @@ import { client } from '../../client' import { escapeChat } from '../../encoding'; import setCookie from '../../utils/setCookie'; - +import { saveChatlogHandle } from '../../client/saveChatLogHandle' /** * Sends an out-of-character chat message. * @param {string} message the message to send @@ -17,7 +17,7 @@ export const sendOOC = (message: string) => { const oocMessage = `${escapeChat(message)}`; const commands = { - "/save_chatlog": client.saveChatlogHandle, + "/save_chatlog": saveChatlogHandle, }; const commandsMap = new Map(Object.entries(commands)); diff --git a/webAO/client/sender/sendServer.ts b/webAO/client/sender/sendServer.ts index a9da3bd..7678381 100644 --- a/webAO/client/sender/sendServer.ts +++ b/webAO/client/sender/sendServer.ts @@ -6,5 +6,6 @@ let { mode } = queryParser() * @param {string} message the message to send */ export const sendServer = (message: string) => { + console.debug("C: "+message) mode === "replay" ? client.sender.sendSelf(message) : client.serv.send(message); }
\ No newline at end of file diff --git a/webAO/client/setEmote.ts b/webAO/client/setEmote.ts index 161eb51..f4fbdbb 100644 --- a/webAO/client/setEmote.ts +++ b/webAO/client/setEmote.ts @@ -28,7 +28,7 @@ const setEmote = async ( for (const extension of extensionsMap) { // Hides all sprites before creating a new sprite - if (client.viewport.lastChar !== client.viewport.chatmsg.name) { + if (client.viewport.getLastCharacter() !== client.viewport.getChatmsg().name) { emoteSelector.src = transparentPng; } let url; 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 = (<HTMLInputElement>( + 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/dom/changeMusicVolume.ts b/webAO/dom/changeMusicVolume.ts new file mode 100644 index 0000000..9e5d51a --- /dev/null +++ b/webAO/dom/changeMusicVolume.ts @@ -0,0 +1,14 @@ +import { client } from '../client' +import setCookie from '../utils/setCookie'; + +export const changeMusicVolume = (volume: number = -1) => { + const clientVolume = Number( + (<HTMLInputElement>document.getElementById("client_mvolume")).value + ); + let musicVolume = volume === -1 ? clientVolume : volume; + client.viewport.music.forEach( + (channel: HTMLAudioElement) => (channel.volume = musicVolume) + ); + setCookie("musicVolume", String(musicVolume)); +}; +window.changeMusicVolume = changeMusicVolume;
\ No newline at end of file diff --git a/webAO/dom/iniEdit.ts b/webAO/dom/iniEdit.ts index 359a226..b26c179 100644 --- a/webAO/dom/iniEdit.ts +++ b/webAO/dom/iniEdit.ts @@ -1,14 +1,18 @@ import { client } from "../client"; +import { handleCharacterInfo } from "../client/handleCharacterInfo"; import { packetHandler } from "../packets/packetHandler"; /** * Triggered by the ini button. */ export async function iniedit() { - const ininame = (<HTMLInputElement>document.getElementById("client_ininame")) - .value; + const iniselect = (<HTMLSelectElement>document.getElementById("client_iniselect")) + const ininame = (<HTMLInputElement>document.getElementById("client_ininame")); const inicharID = client.charID; - await client.handleCharacterInfo(ininame.split("&"), inicharID); + + const newname = iniselect.selectedIndex === 0 ? ininame.value : iniselect.value; + + await handleCharacterInfo(newname.split("&"), inicharID); packetHandler.get("PV")!(`PV#0#CID#${inicharID}`.split("#")); } window.iniedit = iniedit; diff --git a/webAO/dom/muteListClick.ts b/webAO/dom/muteListClick.ts new file mode 100644 index 0000000..e7c9357 --- /dev/null +++ b/webAO/dom/muteListClick.ts @@ -0,0 +1,19 @@ +import { client } from "../client"; +/** + * Triggered when a character in the mute list is clicked + * @param {MouseEvent} event + */ +export function mutelist_click(_event: Event) { + const mutelist = <HTMLSelectElement>(document.getElementById('mute_select')); + const selected_character = mutelist.options[mutelist.selectedIndex]; + + if (client.chars[selected_character.value].muted === false) { + client.chars[selected_character.value].muted = true; + selected_character.text = `${client.chars[selected_character.value].name} (muted)`; + console.info(`muted ${client.chars[selected_character.value].name}`); + } else { + client.chars[selected_character.value].muted = false; + selected_character.text = client.chars[selected_character.value].name; + } +} +window.mutelist_click = mutelist_click;
\ No newline at end of file diff --git a/webAO/dom/opusCheck.ts b/webAO/dom/opusCheck.ts index 939fdc6..5f0248d 100644 --- a/webAO/dom/opusCheck.ts +++ b/webAO/dom/opusCheck.ts @@ -9,7 +9,7 @@ export function opusCheck( if (audio === "") { return; } - console.info(`failed to load sound ${channel.src}`); + console.warn(`failed to load sound ${channel.src}`); let oldsrc = ""; let newsrc = ""; oldsrc = channel.src; diff --git a/webAO/dom/reloadTheme.ts b/webAO/dom/reloadTheme.ts new file mode 100644 index 0000000..bfa46b6 --- /dev/null +++ b/webAO/dom/reloadTheme.ts @@ -0,0 +1,16 @@ +import { client } from '../client' +import setCookie from '../utils/setCookie'; + +/** + * Triggered by the theme selector. + */ +export const reloadTheme = () => { + client.viewport.setTheme((<HTMLSelectElement>document.getElementById("client_themeselect")) + .value); + + setCookie("theme", client.viewport.getTheme()); + (<HTMLAnchorElement>( + document.getElementById("client_theme") + )).href = `styles/${client.viewport.getTheme()}.css`; +} +window.reloadTheme = reloadTheme;
\ No newline at end of file diff --git a/webAO/dom/resizeChatbox.ts b/webAO/dom/resizeChatbox.ts index efb8bdc..887877d 100644 --- a/webAO/dom/resizeChatbox.ts +++ b/webAO/dom/resizeChatbox.ts @@ -17,7 +17,6 @@ export function resizeChatbox() { let weekday = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; const month = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; document.getElementById("client_clock_month")!.innerText = month[now.getMonth()]; - console.debug(CHATBOX); if (CHATBOX == "acww") { weekday = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]; document.getElementById("client_clock_weekday")!.innerText = weekday[now.getDay()]; diff --git a/webAO/dom/setChatbox.ts b/webAO/dom/setChatbox.ts index 6d1a78c..095ead3 100644 --- a/webAO/dom/setChatbox.ts +++ b/webAO/dom/setChatbox.ts @@ -5,7 +5,7 @@ import setCookie from "../utils/setCookie"; /** * Set the style of the chatbox */ -export function setChatbox(style: string) { +export function setChatbox(setstyle: string) { const chatbox_theme = <HTMLAnchorElement>( document.getElementById("chatbox_theme") ); @@ -16,6 +16,7 @@ export function setChatbox(style: string) { setCookie("chatbox", CHATBOX); if (CHATBOX === "dynamic") { + const style = setstyle.replace("chat",""); if (chatbox_arr.includes(style)) { chatbox_theme.href = `styles/chatbox/${style}.css`; } else { diff --git a/webAO/dom/showNameClick.ts b/webAO/dom/showNameClick.ts index 265f6c8..3e48b70 100644 --- a/webAO/dom/showNameClick.ts +++ b/webAO/dom/showNameClick.ts @@ -5,7 +5,7 @@ import setCookie from "../utils/setCookie"; * Triggered when the showname checkboc is clicked * @param {MouseEvent} event */ -export function showname_click(_event: Event) { +export function showname_click(_event: Event | null) { setCookie( "showname", String((<HTMLInputElement>document.getElementById("showname")).checked) diff --git a/webAO/dom/twofactor.ts b/webAO/dom/twofactor.ts new file mode 100644 index 0000000..b7e947a --- /dev/null +++ b/webAO/dom/twofactor.ts @@ -0,0 +1,10 @@ +import { client } from "../client"; + +function handleCredentialResponse(response: any) { + client.sender.sendServer(`2T#${response.credential}#%`); + } +window.handleCredentialResponse = handleCredentialResponse; + +export function showFactorDialog(args: string[]) { + document.getElementById("client_secondfactor").style.display = args[1]; +}
\ No newline at end of file diff --git a/webAO/dom/updateIniswap.ts b/webAO/dom/updateIniswap.ts new file mode 100644 index 0000000..5bea0f5 --- /dev/null +++ b/webAO/dom/updateIniswap.ts @@ -0,0 +1,18 @@ +/** + * Update iniswap drowdown + */ +export function updateIniswap() { + const ini_select = <HTMLSelectElement>( + document.getElementById("client_iniselect") + ); + const ini_name = <HTMLInputElement>( + document.getElementById("client_ininame") + ); + + if (ini_select.selectedIndex === 0) { + ini_name.style.display = "initial"; + } else { + ini_name.style.display = "none"; + } +} +window.updateIniswap = updateIniswap; diff --git a/webAO/dom/window.ts b/webAO/dom/window.ts index 2535768..0b3bd34 100644 --- a/webAO/dom/window.ts +++ b/webAO/dom/window.ts @@ -17,6 +17,7 @@ declare global { changeBackgroundOOC: () => void; updateActionCommands: (side: string) => void; updateEvidenceIcon: () => void; + updateIniswap: () => void; resizeChatbox: () => void; setChatbox: (style: string) => void; getIndexFromSelect: (select_box: string, value: string) => Number; @@ -51,6 +52,7 @@ declare global { onEnter: (event: any) => void; onReplayGo: (_event: any) => void; onOOCEnter: (_event: any) => void; + handleCredentialResponse: (_event: any) => void; } } export { }
\ No newline at end of file diff --git a/webAO/encoding.ts b/webAO/encoding.ts index 54770d0..3477d7b 100644 --- a/webAO/encoding.ts +++ b/webAO/encoding.ts @@ -4,10 +4,10 @@ */ export function escapeChat(estring: string): string { return estring - .replace(/#/g, '<num>') - .replace(/&/g, '<and>') - .replace(/%/g, '<percent>') - .replace(/\$/g, '<dollar>'); + .replaceAll('#', '<num>') + .replaceAll('&', '<and>') + .replaceAll('%', '<percent>') + .replaceAll('$', '<dollar>'); } /** @@ -16,10 +16,10 @@ export function escapeChat(estring: string): string { */ export function unescapeChat(estring: string): string { return estring - .replace(/<num>/g, '#') - .replace(/<and>/g, '&') - .replace(/<percent>/g, '%') - .replace(/<dollar>/g, '$'); + .replaceAll('<num>', '#') + .replaceAll('<and>', '&') + .replaceAll('<percent>', '%') + .replaceAll('<dollar>', '$'); } /** @@ -31,8 +31,8 @@ export function unescapeChat(estring: string): string { export function safeTags(unsafe: string): string { if (unsafe) { return unsafe - .replace(/>/g, '>') - .replace(/</g, '<'); + .replaceAll('>', '>') + .replaceAll('<', '<'); } return ''; } diff --git a/webAO/master.ts b/webAO/master.ts index 5a31024..7e9c994 100644 --- a/webAO/master.ts +++ b/webAO/master.ts @@ -20,10 +20,10 @@ let selectedServer: number = -1; let servers: { name: string, description: string, ip: string, port: number, ws_port: number, assets: string, online: string }[] = []; servers[-2] = { - name: 'Singleplayer', description: 'Build cases, try out new things', ip: '127.0.0.1', port: 50001, ws_port: 50001, assets: '', online: '', + name: 'Singleplayer', description: 'Build cases, try out new things', ip: '127.0.0.1', port: 50001, ws_port: 50001, assets: '', online: 'Online: 0/1', }; servers[-1] = { - name: 'Localhost', description: 'This is your computer on port 50001', ip: '127.0.0.1', port: 50001, ws_port: 50001, assets: '', online: 'Online: ?/?', + name: 'Localhost', description: 'This is your computer on port 50001', ip: '127.0.0.1', port: 50001, ws_port: 50001, assets: '', online: 'Offline', }; const fpPromise = FingerprintJS.load(); @@ -58,11 +58,7 @@ export function setServ(ID: number) { if (document.getElementById(`server${ID}`).className === '') { checkOnline(ID, `${servers[ID].ip}:${servers[ID].ws_port}`); } - if (servers[ID].description !== undefined) { - document.getElementById('serverdescription_content').innerHTML = `<b>${servers[ID].online}</b><br>${safeTags(servers[ID].description)}`; - } else { - document.getElementById('serverdescription_content').innerHTML = ''; - } + document.getElementById('serverdescription_content').innerHTML = `<b>${servers[ID].online}</b><br>${safeTags(servers[ID].description)}`; } window.setServ = setServ; @@ -137,6 +133,7 @@ function processServerlist(thelist: { name: string, description: string, ip: str const serverEntry: { name: string, description: string, ip: string, port: number, ws_port: number, assets: string, online: string } = thelist[i]; servers[i] = serverEntry; + servers[i].online = "Offline"; const ipport = `${serverEntry.ip}:${serverEntry.ws_port}`; diff --git a/webAO/packets/handlers/handleASS.ts b/webAO/packets/handlers/handleASS.ts index ea8a0d3..bfd764d 100644 --- a/webAO/packets/handlers/handleASS.ts +++ b/webAO/packets/handlers/handleASS.ts @@ -6,5 +6,6 @@ import { setAOhost } from "../../client/aoHost"; * @param {Array} args packet arguments */ export const handleASS = (args: string[]) => { - setAOhost(args[1]); + if(args[1] !== "None") + setAOhost(args[1]); } diff --git a/webAO/packets/handlers/handleBD.ts b/webAO/packets/handlers/handleBD.ts index 4ec291e..dbfb54b 100644 --- a/webAO/packets/handlers/handleBD.ts +++ b/webAO/packets/handlers/handleBD.ts @@ -1,5 +1,6 @@ -import { client, setBanned } from "../../client"; +import { setBanned } from "../../client"; import { safeTags } from "../../encoding"; +import { handleBans } from '../../client/handleBans' /** @@ -8,6 +9,6 @@ import { safeTags } from "../../encoding"; * @param {Array} args ban reason */ export const handleBD = (args: string[]) => { - client.handleBans("Banned", safeTags(args[1])); + handleBans("Banned", safeTags(args[1])); setBanned(true); }
\ No newline at end of file diff --git a/webAO/packets/handlers/handleCI.ts b/webAO/packets/handlers/handleCI.ts index 53e42f7..cb693bc 100644 --- a/webAO/packets/handlers/handleCI.ts +++ b/webAO/packets/handlers/handleCI.ts @@ -1,4 +1,5 @@ import { client } from '../../client' +import { handleCharacterInfo } from '../../client/handleCharacterInfo' /** * Handles incoming character information, bundling multiple characters * per packet. @@ -18,7 +19,7 @@ export const handleCI = (args: string[]) => { (<HTMLProgressElement>( document.getElementById("client_loadingbar") )).value = charid; - setTimeout(() => client.handleCharacterInfo(chargs, charid), 500); + setTimeout(() => handleCharacterInfo(chargs, charid), 500); } } // Request the next pack diff --git a/webAO/packets/handlers/handleEM.ts b/webAO/packets/handlers/handleEM.ts index 9f3cb87..cfe63b7 100644 --- a/webAO/packets/handlers/handleEM.ts +++ b/webAO/packets/handlers/handleEM.ts @@ -1,4 +1,8 @@ import { client } from '../../client' +import { addTrack } from '../../client/addTrack'; +import { createArea } from '../../client/createArea'; +import { fix_last_area } from '../../client/fixLastArea'; +import { isAudio } from '../../client/isAudio'; import { safeTags } from '../../encoding'; /** @@ -16,20 +20,20 @@ export const handleEM = (args: string[]) => { for (let i = 2; i < args.length - 1; i++) { if (i % 2 === 0) { - const trackname = safeTags(args[i]); + const trackname = args[i]; const trackindex = Number(args[i - 1]); (<HTMLProgressElement>( document.getElementById("client_loadingbar") )).value = client.char_list_length + client.evidence_list_length + trackindex; if (client.musics_time) { - client.addTrack(trackname); - } else if (client.isAudio(trackname)) { + addTrack(trackname); + } else if (isAudio(trackname)) { client.musics_time = true; - client.fix_last_area(); - client.addTrack(trackname); + fix_last_area(); + addTrack(trackname); } else { - client.createArea(trackindex, trackname); + createArea(trackindex, trackname); } } } diff --git a/webAO/packets/handlers/handleFA.ts b/webAO/packets/handlers/handleFA.ts index ccfe923..7a373e8 100644 --- a/webAO/packets/handlers/handleFA.ts +++ b/webAO/packets/handlers/handleFA.ts @@ -1,4 +1,5 @@ import { client } from '../../client' +import { createArea } from '../../client/createArea'; import { safeTags } from '../../encoding'; /** @@ -9,6 +10,6 @@ export const handleFA = (args: string[]) => { client.resetAreaList(); for (let i = 1; i < args.length - 1; i++) { - client.createArea(i - 1, safeTags(args[i])); + createArea(i - 1, safeTags(args[i])); } } diff --git a/webAO/packets/handlers/handleFL.ts b/webAO/packets/handlers/handleFL.ts index 378d5a9..89df4ed 100644 --- a/webAO/packets/handlers/handleFL.ts +++ b/webAO/packets/handlers/handleFL.ts @@ -15,13 +15,10 @@ export const handleFL = (args: string[]) => { document.getElementById("textcolor") ); - colorselect.options[colorselect.options.length] = new Option( - "Yellow", - "5" - ); - colorselect.options[colorselect.options.length] = new Option("Grey", "6"); - colorselect.options[colorselect.options.length] = new Option("Pink", "7"); - colorselect.options[colorselect.options.length] = new Option("Cyan", "8"); + colorselect.options[colorselect.options.length] = new Option("Yellow","5"); + colorselect.options[colorselect.options.length] = new Option("Pink", "6"); + colorselect.options[colorselect.options.length] = new Option("Cyan", "7"); + colorselect.options[colorselect.options.length] = new Option("Grey", "8"); } if (args.includes("cccc_ic_support")) { diff --git a/webAO/packets/handlers/handleFM.ts b/webAO/packets/handlers/handleFM.ts index 630477b..fce10e3 100644 --- a/webAO/packets/handlers/handleFM.ts +++ b/webAO/packets/handlers/handleFM.ts @@ -1,7 +1,7 @@ import { client } from "../../client"; +import { addTrack } from "../../client/addTrack"; import { safeTags } from "../../encoding"; - /** * Handles updated music list * @param {Array} args packet arguments @@ -11,6 +11,6 @@ export const handleFM = (args: string[]) => { for (let i = 1; i < args.length - 1; i++) { // Check when found the song for the first time - client.addTrack(safeTags(args[i])); + addTrack(safeTags(args[i])); } } diff --git a/webAO/packets/handlers/handleKB.ts b/webAO/packets/handlers/handleKB.ts index 8705b83..b0aa2b2 100644 --- a/webAO/packets/handlers/handleKB.ts +++ b/webAO/packets/handlers/handleKB.ts @@ -1,5 +1,6 @@ -import { client, setBanned } from "../../client"; +import { setBanned } from "../../client"; import { safeTags } from "../../encoding"; +import { handleBans } from '../../client/handleBans' /** * Handles the banned packet @@ -7,6 +8,6 @@ import { safeTags } from "../../encoding"; * @param {Array} args ban reason */ export const handleKB = (args: string[]) => { - client.handleBans("Banned", safeTags(args[1])); + handleBans("Banned", safeTags(args[1])); setBanned(true); } diff --git a/webAO/packets/handlers/handleKK.ts b/webAO/packets/handlers/handleKK.ts index fd9a88c..c8a97b1 100644 --- a/webAO/packets/handlers/handleKK.ts +++ b/webAO/packets/handlers/handleKK.ts @@ -1,11 +1,10 @@ -import { client } from "../../client"; import { safeTags } from "../../encoding"; - +import { handleBans } from '../../client/handleBans' /** * Handles the kicked packet * @param {Array} args kick reason */ export const handleKK = (args: string[]) => { - client.handleBans("Kicked", safeTags(args[1])); + handleBans("Kicked", safeTags(args[1])); } diff --git a/webAO/packets/handlers/handleMC.ts b/webAO/packets/handlers/handleMC.ts index bf60eb9..aeb178d 100644 --- a/webAO/packets/handlers/handleMC.ts +++ b/webAO/packets/handlers/handleMC.ts @@ -1,6 +1,7 @@ import { prepChat } from "../../encoding"; -import { appendICLog, client } from '../../client' +import { client } from '../../client' import { AO_HOST } from "../../client/aoHost"; +import { appendICLog } from '../../client/appendICLog' /** * Handles a music change to an arbitrary resource. diff --git a/webAO/packets/handlers/handleMS.ts b/webAO/packets/handlers/handleMS.ts index 0aad19a..c30e777 100644 --- a/webAO/packets/handlers/handleMS.ts +++ b/webAO/packets/handlers/handleMS.ts @@ -1,124 +1,111 @@ -import { client, extrafeatures, resetICParams, UPDATE_INTERVAL } from "../../client"; +import { client, extrafeatures, UPDATE_INTERVAL } from "../../client"; +import { handleCharacterInfo } from "../../client/handleCharacterInfo"; +import { resetICParams } from "../../client/resetICParams"; import { prepChat, safeTags } from "../../encoding"; - +import { handle_ic_speaking } from '../../viewport/utils/handleICSpeaking' /** * Handles an in-character chat message. * @param {*} args packet arguments */ export const handleMS = (args: string[]) => { - // TODO: this if-statement might be a bug. - if (args[4] !== client.viewport.chatmsg.content) { - document.getElementById("client_inner_chat")!.innerHTML = ""; + // TODO: this if-statement might be a bug. + if (args[4] !== client.viewport.getChatmsg().content) { + document.getElementById("client_inner_chat")!.innerHTML = ""; - const char_id = Number(args[9]); - const char_name = safeTags(args[3]); + const char_id = Number(args[9]); + const char_name = safeTags(args[3]); - let msg_nameplate = args[3]; - let msg_blips = "male"; - let char_chatbox = "default"; - let char_muted = false; + let msg_nameplate = args[3]; + let msg_blips = "male"; + let char_chatbox = "default"; + let char_muted = false; - if (char_id < client.char_list_length && char_id >= 0) { - if(client.chars[char_id].name !== char_name) { + if (char_id < client.char_list_length && char_id >= 0) { + if (client.chars[char_id].name !== char_name) { console.info( `${client.chars[char_id].name} is iniediting to ${char_name}` ); const chargs = (`${char_name}&` + "iniediter").split("&"); - client.handleCharacterInfo(chargs, char_id); - } + handleCharacterInfo(chargs, char_id); } + } - try { - msg_nameplate = client.chars[char_id].showname; - } catch (e) { - msg_nameplate = args[3]; - } + try { + msg_nameplate = client.chars[char_id].showname; + } catch (e) { + msg_nameplate = args[3]; + } - try { - msg_blips = client.chars[char_id].blips; - } catch (e) {} + try { + msg_blips = client.chars[char_id].blips; + } catch (e) { } - try { - char_chatbox = client.chars[char_id].chat; - } catch (e) { - char_chatbox = "default"; - } + try { + char_chatbox = client.chars[char_id].chat; + } catch (e) { + char_chatbox = "default"; + } - try { - char_muted = client.chars[char_id].muted; - } catch (e) { - char_muted = false; - console.error("we're still missing some character data"); - } + try { + char_muted = client.chars[char_id].muted; + } catch (e) { + char_muted = false; + console.error("we're still missing some character data"); + } + + if (char_muted === false) { + let chatmsg = { + deskmod: Number(safeTags(args[1]).toLowerCase()), + preanim: safeTags(args[2]).toLowerCase(), // get preanim + nameplate: msg_nameplate, + chatbox: char_chatbox, + name: char_name, + sprite: safeTags(args[4]).toLowerCase(), + content: prepChat(args[5]), // Escape HTML tags + side: args[6].toLowerCase(), + sound: safeTags(args[7]).toLowerCase(), + blips: safeTags(msg_blips), + type: Number(args[8]), + charid: char_id, + snddelay: Number(args[10]), + objection: Number(args[11]), + evidence: Number(safeTags(args[12])), + flip: Number(args[13]), + flash: Number(args[14]), + color: Number(args[15]), + speed: UPDATE_INTERVAL, + }; - if (char_muted === false) { - let chatmsg = { - deskmod: safeTags(args[1]).toLowerCase(), - preanim: safeTags(args[2]).toLowerCase(), // get preanim - nameplate: msg_nameplate, - chatbox: char_chatbox, - name: char_name, - sprite: safeTags(args[4]).toLowerCase(), - content: prepChat(args[5]), // Escape HTML tags - side: args[6].toLowerCase(), - sound: safeTags(args[7]).toLowerCase(), - blips: safeTags(msg_blips), - type: Number(args[8]), - charid: char_id, - snddelay: Number(args[10]), - objection: Number(args[11]), - evidence: safeTags(args[12]), - flip: Number(args[13]), - flash: Number(args[14]), - color: Number(args[15]), - speed: UPDATE_INTERVAL, + if (extrafeatures.includes("cccc_ic_support")) { + const extra_cccc = { + showname: prepChat(args[16]), + other_charid: Number(args[17]), + other_name: safeTags(args[18]), + other_emote: safeTags(args[19]), + self_offset: args[20].split("<and>"), // HACK: here as well, client is fucked and uses this instead of & + other_offset: args[21].split("<and>"), + other_flip: Number(args[22]), + noninterrupting_preanim: Number(args[23]), }; + chatmsg = Object.assign(extra_cccc, chatmsg); - if (extrafeatures.includes("cccc_ic_support")) { - const extra_cccc = { - showname: safeTags(args[16]), - other_charid: Number(args[17]), - other_name: safeTags(args[18]), - other_emote: safeTags(args[19]), - self_offset: args[20].split("<and>"), // HACK: here as well, client is fucked and uses this instead of & - other_offset: args[21].split("<and>"), - other_flip: Number(args[22]), - noninterrupting_preanim: Number(args[23]), + if (extrafeatures.includes("looping_sfx")) { + const extra_27 = { + looping_sfx: Number(args[24]), + screenshake: Number(args[25]), + frame_screenshake: safeTags(args[26]), + frame_realization: safeTags(args[27]), + frame_sfx: safeTags(args[28]), }; - chatmsg = Object.assign(extra_cccc, chatmsg); + chatmsg = Object.assign(extra_27, chatmsg); - if (extrafeatures.includes("looping_sfx")) { - const extra_27 = { - looping_sfx: Number(args[24]), - screenshake: Number(args[25]), - frame_screenshake: safeTags(args[26]), - frame_realization: safeTags(args[27]), - frame_sfx: safeTags(args[28]), + if (extrafeatures.includes("effects")) { + const extra_28 = { + additive: Number(args[29]), + effects: args[30].split("|"), }; - chatmsg = Object.assign(extra_27, chatmsg); - - if (extrafeatures.includes("effects")) { - const extra_28 = { - additive: Number(args[29]), - effects: args[30].split("|"), - }; - chatmsg = Object.assign(extra_28, chatmsg); - } else { - const extra_28 = { - additive: 0, - effects: ["", "", ""], - }; - chatmsg = Object.assign(extra_28, chatmsg); - } + chatmsg = Object.assign(extra_28, chatmsg); } else { - const extra_27 = { - looping_sfx: 0, - screenshake: 0, - frame_screenshake: "", - frame_realization: "", - frame_sfx: "", - }; - chatmsg = Object.assign(extra_27, chatmsg); const extra_28 = { additive: 0, effects: ["", "", ""], @@ -126,17 +113,6 @@ export const handleMS = (args: string[]) => { chatmsg = Object.assign(extra_28, chatmsg); } } else { - const extra_cccc = { - showname: "", - other_charid: 0, - other_name: "", - other_emote: "", - self_offset: [0, 0], - other_offset: [0, 0], - other_flip: 0, - noninterrupting_preanim: 0, - }; - chatmsg = Object.assign(extra_cccc, chatmsg); const extra_27 = { looping_sfx: 0, screenshake: 0, @@ -151,12 +127,44 @@ export const handleMS = (args: string[]) => { }; chatmsg = Object.assign(extra_28, chatmsg); } + } else { + const extra_cccc = { + showname: "", + other_charid: 0, + other_name: "", + other_emote: "", + self_offset: [0, 0], + other_offset: [0, 0], + other_flip: 0, + noninterrupting_preanim: 0, + }; + chatmsg = Object.assign(extra_cccc, chatmsg); + const extra_27 = { + looping_sfx: 0, + screenshake: 0, + frame_screenshake: "", + frame_realization: "", + frame_sfx: "", + }; + chatmsg = Object.assign(extra_27, chatmsg); + const extra_28 = { + additive: 0, + effects: ["", "", ""], + }; + chatmsg = Object.assign(extra_28, chatmsg); + } - // our own message appeared, reset the buttons - if (chatmsg.charid === client.charID) { - resetICParams(); - } - client.viewport.handle_ic_speaking(chatmsg); // no await + if (chatmsg.content.trim() === "") { + //blankpost + chatmsg.content = ""; } + + // our own message appeared, reset the buttons + if (chatmsg.charid === client.charID) { + resetICParams(); + } + + handle_ic_speaking(chatmsg); // no await } - }
\ No newline at end of file + } +}
\ No newline at end of file diff --git a/webAO/packets/handlers/handleRT.ts b/webAO/packets/handlers/handleRT.ts index 5bbf2b2..62ebb1e 100644 --- a/webAO/packets/handlers/handleRT.ts +++ b/webAO/packets/handlers/handleRT.ts @@ -1,5 +1,5 @@ import { client } from "../../client"; - +import { initTestimonyUpdater } from '../../viewport/utils/initTestimonyUpdater' /** * Handles a testimony states. @@ -21,5 +21,5 @@ export const handleRT = (args: string[]) => { default: console.warn("Invalid testimony"); } - client.viewport.initTestimonyUpdater(); + initTestimonyUpdater(); }
\ No newline at end of file diff --git a/webAO/packets/handlers/handleSC.ts b/webAO/packets/handlers/handleSC.ts index f4953e0..b42a4cd 100644 --- a/webAO/packets/handlers/handleSC.ts +++ b/webAO/packets/handlers/handleSC.ts @@ -1,6 +1,7 @@ import queryParser from "../../utils/queryParser"; import { client } from '../../client' +import { handleCharacterInfo } from "../../client/handleCharacterInfo"; let { mode } = queryParser(); /** @@ -30,7 +31,7 @@ export const handleSC = async (args: string[]) => { document.getElementById("client_loadingbar") )).value = charid; await sleep(0.1); // TODO: Too many network calls without this. net::ERR_INSUFFICIENT_RESOURCES - client.handleCharacterInfo(chargs, charid); + handleCharacterInfo(chargs, charid); } // We're done with the characters, request the music client.sender.sendServer("RM#%"); diff --git a/webAO/packets/handlers/handleSM.ts b/webAO/packets/handlers/handleSM.ts index 48f9cd9..08bf7e0 100644 --- a/webAO/packets/handlers/handleSM.ts +++ b/webAO/packets/handlers/handleSM.ts @@ -1,4 +1,8 @@ import { client } from '../../client' +import { addTrack } from '../../client/addTrack' +import { isAudio } from '../../client/isAudio' +import { fix_last_area } from '../../client/fixLastArea' +import { createArea } from '../../client/createArea' /** * Handles incoming music information, containing all music in one packet. * @param {Array} args packet arguments @@ -21,14 +25,15 @@ export const handleSM = (args: string[]) => { document.getElementById("client_loadingbar") )).value = client.char_list_length + client.evidence_list_length + i; if (client.musics_time) { - client.addTrack(trackname); - } else if (client.isAudio(trackname)) { + addTrack(trackname); + } else if (isAudio(trackname)) { client.musics_time = true; - client.fix_last_area(); - client.addTrack(trackname); + fix_last_area(); + addTrack(trackname); } else { - client.createArea(trackindex, trackname); + createArea(trackindex, trackname); } + } // Music done, carry on diff --git a/webAO/packets/handlers/handleackMS.ts b/webAO/packets/handlers/handleackMS.ts index 2b971b0..dcca118 100644 --- a/webAO/packets/handlers/handleackMS.ts +++ b/webAO/packets/handlers/handleackMS.ts @@ -1,4 +1,4 @@ -import { resetICParams } from "../../client"; +import { resetICParams } from '../../client/resetICParams' /** * server got our message diff --git a/webAO/packets/packets.ts b/webAO/packets/packets.ts index 79c43c1..c9d0bb8 100644 --- a/webAO/packets/packets.ts +++ b/webAO/packets/packets.ts @@ -2,6 +2,7 @@ import { handleMS } from './handlers/handleMS'; import { handleCT } from './handlers/handleCT' import { handleMC } from './handlers/handleMC' import { handleRMC } from './handlers/handleRMC' +import { showFactorDialog } from '../dom/twofactor' import { handleFL } from './handlers/handleFL' import { handleLE } from './handlers/handleLE' import { handleEM } from './handlers/handleEM' @@ -47,6 +48,7 @@ export const packets = { "CI": handleCI, "SC": handleSC, "EI": handleEI, + "2A": showFactorDialog, "FL": handleFL, "LE": handleLE, "EM": handleEM, diff --git a/webAO/styles/chatbox/chat999.css b/webAO/styles/chatbox/999.css index de20d28..de20d28 100644 --- a/webAO/styles/chatbox/chat999.css +++ b/webAO/styles/chatbox/999.css diff --git a/webAO/styles/chatbox/chatboxes.js b/webAO/styles/chatbox/chatboxes.js index 0e0367f..bc73109 100644 --- a/webAO/styles/chatbox/chatboxes.js +++ b/webAO/styles/chatbox/chatboxes.js @@ -1,21 +1,21 @@ export default [ 'aa', 'acww', - 'chatdd', + 'dd', 'dgs', - 'chatplvsaa', + 'plvsaa', 'trilogy', - 'chatfuture', + 'future', 'legacy', 'ddlc', 'dr1', - 'chatdr2', + 'dr2', 'drv3', 'drae', - 'chatp3', + 'p3', 'p4', 'p5', - 'chat999', + '999', 'halla', 'homestuck', 'key', diff --git a/webAO/styles/chatbox/chatdd.css b/webAO/styles/chatbox/dd.css index d1561a4..d1561a4 100644 --- a/webAO/styles/chatbox/chatdd.css +++ b/webAO/styles/chatbox/dd.css diff --git a/webAO/styles/chatbox/chatdr2.css b/webAO/styles/chatbox/dr2.css index 59b11dc..c6701d1 100644 --- a/webAO/styles/chatbox/chatdr2.css +++ b/webAO/styles/chatbox/dr2.css @@ -123,5 +123,9 @@ } #client_trackstatus { - display: fuck; + display: block; +} + +#client_clock { + display: none; }
\ No newline at end of file diff --git a/webAO/styles/chatbox/chatfuture.css b/webAO/styles/chatbox/future.css index cd638f4..cd638f4 100644 --- a/webAO/styles/chatbox/chatfuture.css +++ b/webAO/styles/chatbox/future.css diff --git a/webAO/styles/chatbox/chatp3.css b/webAO/styles/chatbox/p3.css index 8c6a39d..c9234b6 100644 --- a/webAO/styles/chatbox/chatp3.css +++ b/webAO/styles/chatbox/p3.css @@ -114,4 +114,8 @@ #client_trackstatus { display: none; +} + +#client_clock { + display: none; }
\ No newline at end of file diff --git a/webAO/styles/chatbox/p4.css b/webAO/styles/chatbox/p4.css index 7389fae..e31ffe7 100644 --- a/webAO/styles/chatbox/p4.css +++ b/webAO/styles/chatbox/p4.css @@ -112,4 +112,8 @@ #client_trackstatus { display: none; +} + +#client_clock { + display: none; }
\ No newline at end of file diff --git a/webAO/styles/chatbox/p5.css b/webAO/styles/chatbox/p5.css index da27529..112c521 100644 --- a/webAO/styles/chatbox/p5.css +++ b/webAO/styles/chatbox/p5.css @@ -108,4 +108,8 @@ #client_trackstatus { display: none; +} + +#client_clock { + display: none; }
\ No newline at end of file diff --git a/webAO/styles/chatbox/chatplvsaa.css b/webAO/styles/chatbox/plvsaa.css index a656e9b..a656e9b 100644 --- a/webAO/styles/chatbox/chatplvsaa.css +++ b/webAO/styles/chatbox/plvsaa.css diff --git a/webAO/styles/client.css b/webAO/styles/client.css index 8c7898b..ced8339 100644 --- a/webAO/styles/client.css +++ b/webAO/styles/client.css @@ -21,15 +21,12 @@ .text_rainbow { background-color: #fff; - background-image: repeating-linear-gradient(to right, - red 0% 8%, orange 8% 16%, yellow 16% 24%, green 24% 32%, blue 32% 40%, - red 40% 48%, orange 48% 56%, yellow 56% 64%, green 64% 72%, blue 72% 80%, - red 80% 88%, orange 88% 96%, yellow 96% 100%); - background-size: 40% 40%; + background-image: repeating-linear-gradient(to right, red 0% 20%, orange 20% 40%, yellow 40% 60%, green 60% 80%, blue 80% 100%); + background-size: 75% 75%; background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; - animation: rainbow 4s linear infinite; + animation: rainbow 2s linear infinite; } @keyframes rainbow_alt { @@ -75,6 +72,17 @@ animation: error_blink 3s ease-in-out infinite; } +#client_secondfactor { + display: block; + flex-direction: column; + justify-content: center; + text-align: center; + align-items: center; + background: black; + color: lightgreen; + font-size: large; +} + #client_waiting { display: block; flex-direction: column; diff --git a/webAO/viewport.ts b/webAO/viewport.ts deleted file mode 100644 index 6c39784..0000000 --- a/webAO/viewport.ts +++ /dev/null @@ -1,1091 +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 { appendICLog } from "./client"; -import { checkCallword } from "./client"; -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"; -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<HTMLAudioElement>; - 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<HTMLAudioElement>; - - 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 = <HTMLImageElement>( - document.getElementById(`client_${position}_bench`) - ); - } else { - bench = <HTMLImageElement>document.getElementById("client_bench_classic"); - } - - let court: HTMLImageElement; - if ("def,pro,wit".includes(position)) { - court = <HTMLImageElement>( - document.getElementById(`client_court_${position}`) - ); - } else { - court = <HTMLImageElement>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 = <HTMLImageElement>( - 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 = <HTMLImageElement>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 = - (<HTMLInputElement>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 = <HTMLImageElement>( - 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") ) { - (<HTMLLinkElement>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) - ) { - (<HTMLLinkElement>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 = <HTMLImageElement>document.getElementById("client_evi"); - const shoutSprite = <HTMLImageElement>( - document.getElementById("client_shout") - ); - const effectlayer = <HTMLImageElement>document.getElementById("client_fg"); - const chatBoxInner = document.getElementById("client_inner_chat"); - let charLayers = <HTMLImageElement>document.getElementById("client_char"); - let pairLayers = <HTMLImageElement>( - 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 = <HTMLImageElement>( - document.getElementById(`client_${chatmsg.side}_char`) - ); - pairLayers = <HTMLImageElement>( - 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 = (<HTMLSelectElement>document.getElementById("client_themeselect")) - .value; - - setCookie("theme", theme); - (<HTMLAnchorElement>( - document.getElementById("client_theme") - )).href = `styles/${theme}.css`; - } - window.reloadTheme = reloadTheme; - /** - * Triggered by the blip volume slider. - */ - function changeBlipVolume() { - const blipVolume = (<HTMLInputElement>( - 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( - (<HTMLInputElement>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..4858081 --- /dev/null +++ b/webAO/viewport/constants/colors.ts @@ -0,0 +1,12 @@ +export const COLORS = [ + "white", + "green", + "red", + "orange", + "blue", + "yellow", + "pink", + "cyan", + "grey", + "rainbow", + ];
\ 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..6b96c6e --- /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..5b428c1 --- /dev/null +++ b/webAO/viewport/interfaces/Viewport.ts @@ -0,0 +1,43 @@ +import { ChatMsg } from "./ChatMsg"; + +export interface Viewport { + getTextNow: Function; + setTextNow: Function; + getChatmsg: Function; + setChatmsg: Function; + getSfxPlayed: Function; + setSfxPlayed: Function; + setTickTimer: Function; + getTickTimer: Function; + getAnimating: Function; + setAnimating: Function; + getLastEvidence: Function; + setLastEvidence: Function; + setLastCharacter: Function; + getLastCharacter: Function; + setShoutTimer: Function; + getShoutTimer: Function; + setTestimonyTimer: Function; + getTestimonyTimer: Function; + setTestimonyUpdater: Function; + getTestimonyUpdater: Function; + getTheme: Function; + setTheme: Function; + testimonyAudio: HTMLAudioElement; + chat_tick: Function; + playSFX: Function; + set_side: Function; + updateTestimony: Function; + disposeTestimony: Function; + handleTextTick: Function; + setSfxAudio: Function; + getSfxAudio: Function; + getBackgroundFolder: Function; + blipChannels: HTMLAudioElement[]; + music: any; + musicVolume: number; + setBackgroundName: Function; + getBackgroundName: Function; + shoutaudio: HTMLAudioElement; + updater: any; +} 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<HTMLAudioElement>; + + 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<HTMLAudioElement>; + 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/utils/handleICSpeaking.ts b/webAO/viewport/utils/handleICSpeaking.ts new file mode 100644 index 0000000..e2d147d --- /dev/null +++ b/webAO/viewport/utils/handleICSpeaking.ts @@ -0,0 +1,311 @@ +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 { 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"; + +const attorneyMarkdown = mlConfig(AO_HOST); + +export let startFirstTickCheck: boolean; +export const setStartFirstTickCheck = (val: boolean) => {startFirstTickCheck = val} +export let startSecondTickCheck: boolean; +export const setStartSecondTickCheck = (val: boolean) => {startSecondTickCheck = val} +export let startThirdTickCheck: boolean; +export const setStartThirdTickCheck = (val: boolean) => {startThirdTickCheck = val} +/** + * 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 + */ +export const handle_ic_speaking = async (playerChatMsg: ChatMsg) => { + client.viewport.setChatmsg(playerChatMsg); + client.viewport.setTextNow(""); + client.viewport.setSfxPlayed(0); + client.viewport.setTickTimer(0); + client.viewport.setAnimating(true); + + startFirstTickCheck = true; + startSecondTickCheck = false; + startThirdTickCheck = false; + let charLayers = document.getElementById("client_char")!; + let pairLayers = document.getElementById("client_pair_char")!; + // stop updater + clearTimeout(client.viewport.updater); + + // stop last sfx from looping any longer + client.viewport.getSfxAudio().loop = false; + + const fg = <HTMLImageElement>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 (client.viewport.getLastEvidence() !== client.viewport.getChatmsg().evidence) { + eviBox.style.opacity = "0"; + eviBox.style.height = "0%"; + } + client.viewport.setLastEvidence(client.viewport.getChatmsg().evidence); + + const validSides: string[] = ["def", "pro", "wit"]; // these are for the full view pan, the other positions use 'client_char' + if (validSides.includes(client.viewport.getChatmsg().side)) { + charLayers = document.getElementById(`client_${client.viewport.getChatmsg().side}_char`); + pairLayers = document.getElementById(`client_${client.viewport.getChatmsg().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 = + (<HTMLInputElement>document.getElementById("showname")).checked && + client.viewport.getChatmsg().showname !== "" + ? client.viewport.getChatmsg().showname! + : client.viewport.getChatmsg().nameplate!; + + // Clear out the last message + chatBoxInner.innerText = client.viewport.getTextNow(); + nameBoxInner.innerText = displayname; + + if (client.viewport.getLastCharacter() !== client.viewport.getChatmsg().name) { + charLayers.style.opacity = "0"; + pairLayers.style.opacity = "0"; + } + + client.viewport.setLastCharacter(client.viewport.getChatmsg().name); + + appendICLog(client.viewport.getChatmsg().content, client.viewport.getChatmsg().showname, client.viewport.getChatmsg().nameplate); + + checkCallword(client.viewport.getChatmsg().content, client.viewport.getSfxAudio()); + + setEmote( + AO_HOST, + client, + client.viewport.getChatmsg().name!.toLowerCase(), + client.viewport.getChatmsg().sprite!, + "(a)", + 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 + ); + } + + // gets which shout shall played + const shoutSprite = <HTMLImageElement>( + document.getElementById("client_shout") + ); + + const shout = SHOUTS[client.viewport.getChatmsg().objection]; + if (shout) { + // Hide message box + chatContainerBox.style.opacity = "0"; + if (client.viewport.getChatmsg().objection === 4) { + shoutSprite.src = `${AO_HOST}characters/${encodeURI( + client.viewport.getChatmsg().name!.toLowerCase() + )}/custom.gif`; + } else { + shoutSprite.src = client.resources[shout].src; + shoutSprite.style.animation = "bubble 700ms steps(10, jump-both)"; + } + shoutSprite.style.opacity = "1"; + + client.viewport.shoutaudio.src = `${AO_HOST}characters/${encodeURI( + client.viewport.getChatmsg().name.toLowerCase() + )}/${shout}.opus`; + client.viewport.shoutaudio.play(); + client.viewport.setShoutTimer(client.resources[shout].duration); + } else { + client.viewport.setShoutTimer(0); + } + + client.viewport.getChatmsg().startpreanim = true; + let gifLength = 0; + + if (client.viewport.getChatmsg().type === 1 && client.viewport.getChatmsg().preanim !== "-") { + //we have a preanim + chatContainerBox.style.opacity = "0"; + + gifLength = await getAnimLength( + `${AO_HOST}characters/${encodeURI( + client.viewport.getChatmsg().name!.toLowerCase() + )}/${encodeURI(client.viewport.getChatmsg().preanim)}` + ); + 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; + const setAside = { + position: client.viewport.getChatmsg().side, + showSpeedLines: false, + showDesk: false, + }; + let skipoffset: boolean = false; + if (client.viewport.getChatmsg().type === 5) { + setAside.showSpeedLines = true; + setAside.showDesk = false; + client.viewport.set_side(setAside); + } else { + switch (Number(client.viewport.getChatmsg().deskmod)) { + case 0: //desk is hidden + setAside.showSpeedLines = false; + setAside.showDesk = false; + client.viewport.set_side(setAside); + break; + case 1: //desk is shown + setAside.showSpeedLines = false; + setAside.showDesk = true; + client.viewport.set_side(setAside); + break; + case 2: //desk is hidden during preanim, but shown during idle/talk + setAside.showSpeedLines = false; + setAside.showDesk = false; + client.viewport.set_side(setAside); + break; + case 3: //opposite of 2 + setAside.showSpeedLines = false; + setAside.showDesk = false; + client.viewport.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; + client.viewport.set_side(setAside); + skipoffset = true; + break; + case 5: //opposite of 4 + setAside.showSpeedLines = false; + setAside.showDesk = true; + client.viewport.set_side(setAside); + break; + default: + setAside.showSpeedLines = false; + setAside.showDesk = true; + client.viewport.set_side(setAside); + break; + } + } + + setChatbox(client.viewport.getChatmsg().chatbox); + resizeChatbox(); + + if (!skipoffset) { + // Flip the character + charLayers.style.transform = + client.viewport.getChatmsg().flip === 1 ? "scaleX(-1)" : "scaleX(1)"; + pairLayers.style.transform = + client.viewport.getChatmsg().other_flip === 1 ? "scaleX(-1)" : "scaleX(1)"; + + // Shift by the horizontal offset + switch (client.viewport.getChatmsg().side) { + case "wit": + pairLayers.style.left = `${200 + Number(client.viewport.getChatmsg().other_offset[0])}%`; + charLayers.style.left = `${200 + Number(client.viewport.getChatmsg().self_offset[0])}%`; + break; + case "pro": + pairLayers.style.left = `${400 + Number(client.viewport.getChatmsg().other_offset[0])}%`; + charLayers.style.left = `${400 + Number(client.viewport.getChatmsg().self_offset[0])}%`; + break; + default: + pairLayers.style.left = `${Number(client.viewport.getChatmsg().other_offset[0])}%`; + charLayers.style.left = `${Number(client.viewport.getChatmsg().self_offset[0])}%`; + break; + } + + // New vertical offsets + pairLayers.style.top = `${Number(client.viewport.getChatmsg().other_offset[1])}%`; + charLayers.style.top = `${Number(client.viewport.getChatmsg().self_offset[1])}%`; + } + + client.viewport.blipChannels.forEach( + (channel: HTMLAudioElement) => + (channel.src = `${AO_HOST}sounds/general/sfx-blip${encodeURI( + client.viewport.getChatmsg().blips.toLowerCase() + )}.opus`) + ); + + // process markup + if (client.viewport.getChatmsg().content.startsWith("~~")) { + chatBoxInner.style.textAlign = "center"; + client.viewport.getChatmsg().content = client.viewport.getChatmsg().content.substring(2, client.viewport.getChatmsg().content.length); + } else { + chatBoxInner.style.textAlign = "inherit"; + } + + // apply effects + fg.style.animation = ""; + const effectName = client.viewport.getChatmsg().effects[0].toLowerCase(); + const badEffects = ["", "-", "none"]; + if (effectName.startsWith("rain")) { + (<HTMLLinkElement>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 ( + client.viewport.getChatmsg().effects[0] && + !badEffects.includes(effectName) + ) { + (<HTMLLinkElement>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) => client.viewport.getChatmsg().sound === check)) { + client.viewport.getChatmsg().sound = client.viewport.getChatmsg().effects[2]; + } + + client.viewport.getChatmsg().parsed = await attorneyMarkdown.applyMarkdown( + client.viewport.getChatmsg().content, + + COLORS[client.viewport.getChatmsg().color] + + ); + client.viewport.chat_tick(); +};
\ No newline at end of file diff --git a/webAO/viewport/utils/initTestimonyUpdater.ts b/webAO/viewport/utils/initTestimonyUpdater.ts new file mode 100644 index 0000000..e6f6e9d --- /dev/null +++ b/webAO/viewport/utils/initTestimonyUpdater.ts @@ -0,0 +1,31 @@ +import { Testimony } from '../interfaces/Testimony' +import { client, UPDATE_INTERVAL } from '../../client' +/** + * Intialize testimony updater + */ +export const initTestimonyUpdater = () => { + const testimonyFilenames: Testimony = { + 1: "witnesstestimony", + 2: "crossexamination", + 3: "notguilty", + 4: "guilty", + }; + + const testimony = testimonyFilenames[client.testimonyID]; + if (!testimony) { + console.warn(`Invalid testimony ID ${client.testimonyID}`); + return; + } + + client.viewport.testimonyAudio.src = client.resources[testimony].sfx; + client.viewport.testimonyAudio.play(); + + const testimonyOverlay = <HTMLImageElement>( + document.getElementById("client_testimony") + ); + testimonyOverlay.src = client.resources[testimony].src; + testimonyOverlay.style.opacity = "1"; + + client.viewport.setTestimonyTimer(0); + client.viewport.setTestimonyUpdater(setTimeout(() => client.viewport.updateTestimony(), UPDATE_INTERVAL)); +};
\ No newline at end of file diff --git a/webAO/viewport/utils/setSide.ts b/webAO/viewport/utils/setSide.ts new file mode 100644 index 0000000..3726e83 --- /dev/null +++ b/webAO/viewport/utils/setSide.ts @@ -0,0 +1,90 @@ +import { positions } from '../constants/positions' +import { AO_HOST } from '../../client/aoHost' +import { client } from '../../client' +import tryUrls from '../../utils/tryUrls'; +import fileExists from '../../utils/fileExists'; + +/** + * 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 + */ +export 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 = <HTMLImageElement>( + document.getElementById(`client_${position}_bench`) + ); + } else { + bench = <HTMLImageElement>document.getElementById("client_bench_classic"); + } + + let court: HTMLImageElement; + if ("def,pro,wit".includes(position)) { + court = <HTMLImageElement>( + document.getElementById(`client_court_${position}`) + ); + } else { + court = <HTMLImageElement>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(client.viewport.getBackgroundFolder() + bg); + } + + + if (showDesk === true && desk) { + const deskFilename = (await fileExists(client.viewport.getBackgroundFolder() + desk.ao2)) + ? desk.ao2 + : desk.ao1; + bench.src = client.viewport.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 = ""; + } +}; diff --git a/webAO/viewport/viewport.ts b/webAO/viewport/viewport.ts new file mode 100644 index 0000000..de95030 --- /dev/null +++ b/webAO/viewport/viewport.ts @@ -0,0 +1,495 @@ +import { client, delay } from "../client"; +import { UPDATE_INTERVAL } from "../client"; +import setEmote from "../client/setEmote"; +import { safeTags } from "../encoding"; +import { AO_HOST } from "../client/aoHost"; +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 { Testimony } from './interfaces/Testimony' +import { COLORS } from './constants/colors' +import { set_side } from './utils/setSide' +import { ChatMsg } from "./interfaces/ChatMsg"; +import { setStartFirstTickCheck, setStartSecondTickCheck, startFirstTickCheck, startSecondTickCheck } from "./utils/handleICSpeaking"; + +const viewport = (): Viewport => { + let animating = false; + let blipChannels = createBlipsChannels(); + let chatmsg = defaultChatMsg; + 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 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 getTextNow = () => {return textnow} + const setTextNow = (val: string) => {textnow = val} + const getChatmsg = () => {return chatmsg} + const setChatmsg = (val: ChatMsg) => {chatmsg = val} + const getSfxPlayed = () => sfxplayed + const setSfxPlayed = (val: number) => {sfxplayed = val} + const getTickTimer = () => tickTimer + const setTickTimer = (val: number) => {tickTimer = val} + const getAnimating = () => animating + const setAnimating = (val: boolean) => {animating = val} + const getLastEvidence = () => lastEvi + const setLastEvidence = (val: number) => {lastEvi = val} + const setLastCharacter = (val: string) => {lastChar = val} + const getLastCharacter = () => lastChar + const getShoutTimer = () => shoutTimer + const setShoutTimer = (val: number) => {shoutTimer = val} + const getTheme = () => theme + const setTheme = (val: string) => {theme = val} + const getTestimonyTimer = () => testimonyTimer; + const setTestimonyTimer = (val: number) => {testimonyTimer = val} + const setTestimonyUpdater = (val: any) => {testimonyUpdater = val} + const getTestimonyUpdater = () => testimonyUpdater + const playSFX = async (sfxname: string, looping: boolean) => { + sfxAudio.pause(); + sfxAudio.loop = looping; + sfxAudio.src = sfxname; + sfxAudio.play(); + }; + /** + * 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[client.testimonyID]; + const resource = client.resources[testimony]; + if (!resource) { + disposeTestimony(); + return; + } + + if (testimonyTimer >= resource.duration) { + disposeTestimony(); + } else { + testimonyUpdater = setTimeout(() => updateTestimony(), UPDATE_INTERVAL); + } + }; + /** + * Dispose the testimony overlay + */ + const disposeTestimony = () => { + client.testimonyID = 0; + testimonyTimer = 0; + document.getElementById("client_testimony").style.opacity = "0"; + clearTimeout(testimonyUpdater); + }; + 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 = <HTMLImageElement>document.getElementById("client_evi"); + const shoutSprite = <HTMLImageElement>( + document.getElementById("client_shout") + ); + const effectlayer = <HTMLImageElement>document.getElementById("client_fg"); + const chatBoxInner = document.getElementById("client_inner_chat"); + let charLayers = <HTMLImageElement>document.getElementById("client_char"); + let pairLayers = <HTMLImageElement>( + 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 = <HTMLImageElement>( + document.getElementById(`client_${chatmsg.side}_char`) + ); + pairLayers = <HTMLImageElement>( + 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 + setStartFirstTickCheck(false) + setStartSecondTickCheck(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( + client.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; + }; + + return { + getTextNow, + setTextNow, + getChatmsg, + setChatmsg, + getSfxPlayed, + setSfxPlayed, + setTickTimer, + getTickTimer, + setAnimating, + getAnimating, + getLastEvidence, + setLastEvidence, + setLastCharacter, + getLastCharacter, + getShoutTimer, + setShoutTimer, + setTheme, + getTheme, + setTestimonyTimer, + getTestimonyTimer, + setTestimonyUpdater, + getTestimonyUpdater, + testimonyAudio, + chat_tick, + playSFX, + set_side, + setBackgroundName, + updateTestimony, + disposeTestimony, + handleTextTick, + getBackgroundFolder, + getBackgroundName, + getSfxAudio, + setSfxAudio, + blipChannels, + music, + musicVolume, + shoutaudio, + updater, + }; +}; + +export default viewport; |
