From a7facd6e825e3a2d60752df0b8526482b19a12de Mon Sep 17 00:00:00 2001 From: "caleb.mabry.15@cnu.edu" Date: Wed, 23 Mar 2022 14:40:25 -0400 Subject: Added support for custom backgrounds --- webAO/client.js | 51 ++++++++++++++++++++++++------------------------ webAO/client/setEmote.js | 6 +++--- webAO/utils/tryUrls.js | 20 +++++++++++++++++++ 3 files changed, 49 insertions(+), 28 deletions(-) create mode 100644 webAO/utils/tryUrls.js diff --git a/webAO/client.js b/webAO/client.js index 14bfd08..dd7989b 100644 --- a/webAO/client.js +++ b/webAO/client.js @@ -6,7 +6,7 @@ import FingerprintJS from '@fingerprintjs/fingerprintjs'; import { EventEmitter } from 'events'; -import fileExistsSync from './utils/fileExistsSync'; +import tryUrls from './utils/tryUrls' import { escapeChat, encodeChat, prepChat, safeTags, } from './encoding.js'; @@ -1220,11 +1220,12 @@ class Client extends EventEmitter { document.getElementById('client_charselect').style.display = 'none'; } } - + /** * Handles a background change. * @param {Array} args packet arguments */ + handleBN(args) { viewport.bgname = safeTags(args[1]); const bgfolder = viewport.bgFolder; @@ -1234,19 +1235,17 @@ class Client extends EventEmitter { if (bg_index === 0) { document.getElementById('bg_filename').value = viewport.bgname; } - document.getElementById('bg_preview').src = `${AO_HOST}background/${encodeURI(args[1].toLowerCase())}/defenseempty.png`; - - document.getElementById('client_def_bench').src = `${bgfolder}defensedesk.png`; - document.getElementById('client_wit_bench').src = `${bgfolder}stand.png`; - document.getElementById('client_pro_bench').src = `${bgfolder}prosecutiondesk.png`; - - document.getElementById('client_court').src = `${bgfolder}full.png`; - - document.getElementById('client_court_def').src = `${bgfolder}defenseempty.png`; - document.getElementById('client_court_deft').src = `${bgfolder}transition_def.png`; - document.getElementById('client_court_wit').src = `${bgfolder}witnessempty.png`; - document.getElementById('client_court_prot').src = `${bgfolder}transition_pro.png`; - document.getElementById('client_court_pro').src = `${bgfolder}prosecutorempty.png`; + + tryUrls(`${AO_HOST}background/${encodeURI(args[1].toLowerCase())}/defenseempty`).then(resp => {document.getElementById('bg_preview').src = resp}); + tryUrls(`${bgfolder}defensedesk`).then((resp) => document.getElementById('client_def_bench').src = resp); + tryUrls(`${bgfolder}stand`).then(resp => {document.getElementById('client_wit_bench').src = resp}); + tryUrls(`${bgfolder}prosecutiondesk`).then(resp => {document.getElementById('client_pro_bench').src = resp}); + tryUrls(`${bgfolder}full`).then(resp => {document.getElementById('client_court').src = resp}); + tryUrls(`${bgfolder}defenseempty`).then(resp => {document.getElementById('client_court_def').src = resp}); + tryUrls(`${bgfolder}transition_def`).then(resp => {document.getElementById('client_court_deft').src = resp}); + tryUrls(`${bgfolder}witnessempty`).then(resp => {document.getElementById('client_court_wit').src = resp}); + tryUrls(`${bgfolder}transition_pro`).then(resp => {document.getElementById('client_court_prot').src = resp}); + tryUrls(`${bgfolder}prosecutorempty`).then(resp => {document.getElementById('client_court_pro').src = resp}); if (this.charID === -1) { viewport.changeBackground('jud'); @@ -1774,42 +1773,42 @@ class Viewport { const positions = { def: { - bg: 'defenseempty.png', + bg: 'defenseempty', desk: { ao2: 'defensedesk.png', ao1: 'bancodefensa.png' }, speedLines: 'defense_speedlines.gif', }, pro: { - bg: 'prosecutorempty.png', + bg: 'prosecutorempty', desk: { ao2: 'prosecutiondesk.png', ao1: 'bancoacusacion.png' }, speedLines: 'prosecution_speedlines.gif', }, hld: { - bg: 'helperstand.png', + bg: 'helperstand', desk: null, speedLines: 'defense_speedlines.gif', }, hlp: { - bg: 'prohelperstand.png', + bg: 'prohelperstand', desk: null, speedLines: 'prosecution_speedlines.gif', }, wit: { - bg: 'witnessempty.png', + bg: 'witnessempty', desk: { ao2: 'stand.png', ao1: 'estrado.png' }, speedLines: 'prosecution_speedlines.gif', }, jud: { - bg: 'judgestand.png', + bg: 'judgestand', desk: { ao2: 'judgedesk.png', ao1: 'judgedesk.gif' }, speedLines: 'prosecution_speedlines.gif', }, jur: { - bg: 'jurystand.png', + bg: 'jurystand', desk: { ao2: 'jurydesk.png', ao1: 'estrado.png' }, speedLines: 'defense_speedlines.gif', }, sea: { - bg: 'seancestand.png', + bg: 'seancestand', desk: { ao2: 'seancedesk.png', ao1: 'estrado.png' }, speedLines: 'prosecution_speedlines.gif', }, @@ -1824,7 +1823,7 @@ class Viewport { desk = positions[position].desk; speedLines = positions[position].speedLines; } else { - bg = `${position}.png`; + bg = `${position}`; desk = { ao2: `${position}_overlay.png`, ao1: '_overlay.png' }; speedLines = 'defense_speedlines.gif'; } @@ -1834,7 +1833,9 @@ class Viewport { court.src = `${AO_HOST}themes/default/${encodeURI(speedLines)}`; bench.style.opacity = 0; } else { - court.src = bgfolder + bg; + // Set src here + + court.src = await tryUrls(bgfolder + bg) if (desk) { const deskFilename = await fileExists(bgfolder + desk.ao2) ? desk.ao2 : desk.ao1; bench.src = bgfolder + deskFilename; diff --git a/webAO/client/setEmote.js b/webAO/client/setEmote.js index 4bbaab7..f682fe5 100644 --- a/webAO/client/setEmote.js +++ b/webAO/client/setEmote.js @@ -1,12 +1,12 @@ import transparentPng from '../constants/transparentPng'; -import fileExistsSync from '../utils/fileExistsSync'; +import fileExists from '../utils/fileExists'; /** * Sets all the img tags to the right sources * @param {*} chatmsg */ -const setEmote = (AO_HOST, client, charactername, emotename, prefix, pair, side) => { +const setEmote = async (AO_HOST, client, charactername, emotename, prefix, pair, side) => { const pairID = pair ? 'pair' : 'char'; const characterFolder = `${AO_HOST}characters/`; const acceptedPositions = ['def', 'pro', 'wit']; @@ -30,7 +30,7 @@ const setEmote = (AO_HOST, client, charactername, emotename, prefix, pair, side) } else { url = `${characterFolder}${encodeURI(charactername)}/${encodeURI(prefix)}${encodeURI(emotename)}${extension}`; } - const exists = fileExistsSync(url); + const exists = await fileExists(url); if (exists) { emoteSelector.src = url; break; diff --git a/webAO/utils/tryUrls.js b/webAO/utils/tryUrls.js new file mode 100644 index 0000000..db07ec7 --- /dev/null +++ b/webAO/utils/tryUrls.js @@ -0,0 +1,20 @@ +import fileExists from './fileExists' +import transparentPng from '../constants/transparentPng' +const urlExtensionsToTry = [ + '.png', + '.gif', + '.webp', + '.apng' +] +const tryUrls = async (url) => { + for (let i = 0; i < urlExtensionsToTry.length; i++) { + const extension = urlExtensionsToTry[i] + const fullFileUrl = url + extension + const exists = await fileExists(fullFileUrl); + if (exists) { + return fullFileUrl + } + } + return transparentPng +} +export default tryUrls \ No newline at end of file -- cgit From 415eff55a9db8da9077e0e1dd5d3c3672ff66e8a Mon Sep 17 00:00:00 2001 From: stonedDiscord Date: Wed, 23 Mar 2022 21:45:57 +0100 Subject: remove unused settings --- public/client.html | 27 --------------------------- webAO/client.js | 6 +++--- 2 files changed, 3 insertions(+), 30 deletions(-) diff --git a/public/client.html b/public/client.html index f30e708..28a0bf1 100644 --- a/public/client.html +++ b/public/client.html @@ -507,33 +507,6 @@ placeholder="Put 1 callword per line here" onchange="changeCallwords()">

- ↓ Only touch these settings if you know what you are doing. ↓ -
-
- - -
-
- - -
-
- - -
-
- - -
-
Changing these settings will save them as a cookie.
By doing so, you agree to it being saved.
If you don't agree, disable cookies for this site in your browser.
diff --git a/webAO/client.js b/webAO/client.js index dfba169..6a2947c 100644 --- a/webAO/client.js +++ b/webAO/client.js @@ -9,7 +9,7 @@ import { EventEmitter } from 'events'; import fileExistsSync from './utils/fileExistsSync'; import { escapeChat, encodeChat, prepChat, safeTags, -} from './encoding.js'; +} from './encoding'; import mlConfig from './utils/aoml'; // Load some defaults for the background and evidence dropdowns import vanilla_character_arr from './constants/characters.js'; @@ -2957,7 +2957,7 @@ window.updateActionCommands = updateActionCommands; */ export function changeBackgroundOOC() { const selectedBG = document.getElementById('bg_select'); - const changeBGCommand = document.getElementById('bg_command').value; + const changeBGCommand = "bg $1"; const bgFilename = document.getElementById('bg_filename'); let filename = ''; @@ -2987,7 +2987,7 @@ window.changeRoleOOC = changeRoleOOC; * Random character via OOC. */ export function randomCharacterOOC() { - client.sendOOC(`/${document.getElementById('randomchar_command').value}`); + client.sendOOC(`/randomchar`); } window.randomCharacterOOC = randomCharacterOOC; -- cgit From 89a7862ed462bd952a9814d9d4be078524bcf18c Mon Sep 17 00:00:00 2001 From: stonedDiscord Date: Wed, 23 Mar 2022 21:46:09 +0100 Subject: add strict with a comment --- tsconfig.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tsconfig.json b/tsconfig.json index c2f6882..7803fbe 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,8 @@ "outDir": "./dist", "allowJs": true, "target": "es5", + //"strict": true, + "strictNullChecks": false, //document.getElementBy "downlevelIteration": true }, "include": ["./webAO/*"] -- cgit From e93fa6110bfe510216b558e1b256fac4c267b1a2 Mon Sep 17 00:00:00 2001 From: stonedDiscord Date: Wed, 23 Mar 2022 21:46:28 +0100 Subject: sort of convert encoding to typescript --- webAO/encoding.js | 85 ------------------------------------------------------- webAO/encoding.ts | 64 +++++++++++++++++++++++++++++++++++++++++ webAO/master.ts | 2 +- 3 files changed, 65 insertions(+), 86 deletions(-) delete mode 100644 webAO/encoding.js create mode 100644 webAO/encoding.ts diff --git a/webAO/encoding.js b/webAO/encoding.js deleted file mode 100644 index e6cc3ae..0000000 --- a/webAO/encoding.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Escapes a string to AO1 escape codes. - * @param {string} estring the string to be escaped - */ -export function escapeChat(estring) { - return estring - .replace(/#/g, '') - .replace(/&/g, '') - .replace(/%/g, '') - .replace(/\$/g, ''); -} - -/** - * Unescapes a string to AO1 escape codes. - * @param {string} estring the string to be unescaped - */ -export function unescapeChat(estring) { - return estring - .replace(//g, '#') - .replace(//g, '&') - .replace(//g, '%') - .replace(//g, '$'); -} - -/** - * Escapes a string to be HTML-safe. - * - * XXX: This is unnecessary if we use `createTextNode` instead! - * @param {string} unsafe an unsanitized string - */ -export function safeTags(unsafe) { - if (unsafe) { - return unsafe - .replace(/>/g, '>') - .replace(/ `\\u${(`000${ch.charCodeAt().toString(16)}`).slice(-4)}`); - } if (selectedEncoding === 'utf16') { - // Source: https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String - const buffer = new ArrayBuffer(estring.length * 2); - const result = new Uint16Array(buffer); - for (let i = 0, strLen = estring.length; i < strLen; i++) { - result[i] = estring.charCodeAt(i); - } - return String(result); - } - return estring; -} - -/** - * Decodes text on client side. - * @param {string} estring the string to be decoded - */ -export function decodeChat(estring) { - const selectedDecoding = document.getElementById('client_decoding').value; - if (selectedDecoding === 'unicode') { - // Source: https://stackoverflow.com/questions/7885096/how-do-i-decode-a-string-with-escaped-unicode - return estring.replace(/\\u([\d\w]{1,})/gi, (match, group) => String.fromCharCode(parseInt(group, 16))); - } if (selectedDecoding === 'utf16') { - // Source: https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String - return String.fromCharCode.apply(null, new Uint16Array(estring.split(','))); - } - return estring; -} - -/** - * XXX: a nasty hack made by gameboyprinter. - * @param {string} msg chat message to prepare for display - */ -export function prepChat(msg) { - // TODO: make this less awful - return unescapeChat(decodeChat(msg)); -} diff --git a/webAO/encoding.ts b/webAO/encoding.ts new file mode 100644 index 0000000..930886f --- /dev/null +++ b/webAO/encoding.ts @@ -0,0 +1,64 @@ +/** + * Escapes a string to AO1 escape codes. + * @param {string} estring the string to be escaped + */ +export function escapeChat(estring: string) { + return estring + .replace(/#/g, '') + .replace(/&/g, '') + .replace(/%/g, '') + .replace(/\$/g, ''); +} + +/** + * Unescapes a string to AO1 escape codes. + * @param {string} estring the string to be unescaped + */ +export function unescapeChat(estring: string) { + return estring + .replace(//g, '#') + .replace(//g, '&') + .replace(//g, '%') + .replace(//g, '$'); +} + +/** + * Escapes a string to be HTML-safe. + * + * XXX: This is unnecessary if we use `createTextNode` instead! + * @param {string} unsafe an unsanitized string + */ +export function safeTags(unsafe: string) { + if (unsafe) { + return unsafe + .replace(/>/g, '>') + .replace(/ String.fromCharCode(parseInt(group, 16))); +} + +/** + * XXX: a nasty hack made by gameboyprinter. + * @param {string} msg chat message to prepare for display + */ +export function prepChat(msg: string) { + // TODO: make this less awful + return unescapeChat(decodeChat(msg)); +} diff --git a/webAO/master.ts b/webAO/master.ts index 8fd8779..8c850c2 100644 --- a/webAO/master.ts +++ b/webAO/master.ts @@ -1,6 +1,6 @@ import FingerprintJS from '@fingerprintjs/fingerprintjs'; -import { unescapeChat, safeTags } from './encoding.js'; +import { unescapeChat, safeTags } from './encoding'; declare global { interface Window { -- cgit From 810c1d22b1e52c1662bb3cac518ebfa14ea2d59f Mon Sep 17 00:00:00 2001 From: stonedDiscord Date: Wed, 23 Mar 2022 21:52:05 +0100 Subject: type the encoding functions --- webAO/encoding.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/webAO/encoding.ts b/webAO/encoding.ts index 930886f..1018144 100644 --- a/webAO/encoding.ts +++ b/webAO/encoding.ts @@ -2,7 +2,7 @@ * Escapes a string to AO1 escape codes. * @param {string} estring the string to be escaped */ -export function escapeChat(estring: string) { +export function escapeChat(estring: string): string { return estring .replace(/#/g, '') .replace(/&/g, '') @@ -14,7 +14,7 @@ export function escapeChat(estring: string) { * Unescapes a string to AO1 escape codes. * @param {string} estring the string to be unescaped */ -export function unescapeChat(estring: string) { +export function unescapeChat(estring: string): string { return estring .replace(//g, '#') .replace(//g, '&') @@ -28,7 +28,7 @@ export function unescapeChat(estring: string) { * XXX: This is unnecessary if we use `createTextNode` instead! * @param {string} unsafe an unsanitized string */ -export function safeTags(unsafe: string) { +export function safeTags(unsafe: string): string { if (unsafe) { return unsafe .replace(/>/g, '>') @@ -41,7 +41,7 @@ export function safeTags(unsafe: string) { * Encode text on client side. * @param {string} estring the string to be encoded */ -export function encodeChat(estring: string) { +export function encodeChat(estring: string): string { return estring; } @@ -49,7 +49,7 @@ export function encodeChat(estring: string) { * Decodes text on client side. * @param {string} estring the string to be decoded */ -export function decodeChat(estring: string) { +export function decodeChat(estring: string): string { // Source: https://stackoverflow.com/questions/7885096/how-do-i-decode-a-string-with-escaped-unicode return estring.replace(/\\u([\d\w]{1,})/gi, (match, group) => String.fromCharCode(parseInt(group, 16))); } @@ -58,7 +58,7 @@ export function decodeChat(estring: string) { * XXX: a nasty hack made by gameboyprinter. * @param {string} msg chat message to prepare for display */ -export function prepChat(msg: string) { +export function prepChat(msg: string): string { // TODO: make this less awful return unescapeChat(decodeChat(msg)); } -- cgit From 63410fd71e31e0de21aed41016983146405e2e9d Mon Sep 17 00:00:00 2001 From: stonedDiscord Date: Wed, 23 Mar 2022 21:52:13 +0100 Subject: ao host is a string --- webAO/utils/aoml.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webAO/utils/aoml.ts b/webAO/utils/aoml.ts index da66d0c..154274d 100644 --- a/webAO/utils/aoml.ts +++ b/webAO/utils/aoml.ts @@ -32,7 +32,7 @@ const aomlParser = (text: string) => { return parsed } -const mlConfig = (AO_HOST) => { +const mlConfig = (AO_HOST: string) => { const defaultUrl = `${AO_HOST}themes/default/chat_config.ini` let aomlParsed: Promise<{[key: string]: Aoml}> = request(defaultUrl).then((data) => aomlParser(data)); -- cgit From 93212647c7775358ecd7366423d706a5efae95dc Mon Sep 17 00:00:00 2001 From: stonedDiscord Date: Wed, 23 Mar 2022 22:04:45 +0100 Subject: fix strict errors in master.ts --- package.json | 1 + webAO/master.ts | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index b8e1512..2467ff6 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ }, "dependencies": { "@fingerprintjs/fingerprintjs": "^3.3.3", + "@types/websocket": "^1.0.5", "core-js": "^3.21.1", "golden-layout": "^2.5.0", "regenerator-runtime": "^0.13.9", diff --git a/webAO/master.ts b/webAO/master.ts index 8c850c2..a48f4e9 100644 --- a/webAO/master.ts +++ b/webAO/master.ts @@ -67,10 +67,10 @@ export function setServ(ID: number) { window.setServ = setServ; function checkOnline(serverID: number, coIP: string) { - let oserv; + let serverConnection: WebSocket; if (serverID !== -2) { try { - oserv = new WebSocket(`ws://${coIP}`); + serverConnection = new WebSocket(`ws://${coIP}`); } catch (SecurityError) { document.getElementById(`server${serverID}`).className = 'unavailable'; return; @@ -78,24 +78,24 @@ function checkOnline(serverID: number, coIP: string) { } // define what the callbacks do - function onCOOpen(_e) { + function onCOOpen() { document.getElementById(`server${serverID}`).className = 'available'; - oserv.send(`HI#${hdid}#%`); - oserv.send('ID#webAO#webAO#%'); + serverConnection.send(`HI#${hdid}#%`); + serverConnection.send('ID#webAO#webAO#%'); } - function onCOMessage(e) { + function onCOMessage(e: MessageEvent) { const comsg = e.data; const coheader = comsg.split('#', 2)[0]; const coarguments = comsg.split('#').slice(1); if (coheader === 'PN') { servers[serverID].online = `Online: ${Number(coarguments[0])}/${Number(coarguments[1])}`; - oserv.close(); + serverConnection.close(); return; } if (coheader === 'BD') { servers[serverID].online = 'Banned'; servers[serverID].description = coarguments[0]; - oserv.close(); + serverConnection.close(); return; } if (serverID === selectedServer) { @@ -104,15 +104,15 @@ function checkOnline(serverID: number, coIP: string) { } // assign the callbacks - oserv.onopen = function (evt) { - onCOOpen(evt); + serverConnection.onopen = function () { + onCOOpen(); }; - oserv.onmessage = function (evt) { + serverConnection.onmessage = function (evt: MessageEvent) { onCOMessage(evt); }; - oserv.onerror = function (_evt) { + serverConnection.onerror = function (_evt: Event) { document.getElementById(`server${serverID}`).className = 'unavailable'; }; } -- cgit From b2000f04663092f7a4dfcbab306d67df7e5ce538 Mon Sep 17 00:00:00 2001 From: "caleb.mabry.15@cnu.edu" Date: Thu, 24 Mar 2022 00:22:33 -0400 Subject: Added support for issue with aomlParser --- tsconfig.json | 2 +- webAO/utils/aoml.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 7803fbe..85f6983 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "outDir": "./dist", "allowJs": true, "target": "es5", - //"strict": true, + "strict": true, "strictNullChecks": false, //document.getElementBy "downlevelIteration": true }, diff --git a/webAO/utils/aoml.ts b/webAO/utils/aoml.ts index 154274d..c355edf 100644 --- a/webAO/utils/aoml.ts +++ b/webAO/utils/aoml.ts @@ -1,6 +1,7 @@ import request from "../services/request" interface Aoml { + [key: string]: any | number, name: string; start: string; end: string; -- cgit From cb5117e66c7789db728e80a0b47c2011a46bd6e0 Mon Sep 17 00:00:00 2001 From: "caleb.mabry.15@cnu.edu" Date: Thu, 24 Mar 2022 00:26:33 -0400 Subject: Change any to string --- webAO/utils/aoml.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webAO/utils/aoml.ts b/webAO/utils/aoml.ts index c355edf..fbdee21 100644 --- a/webAO/utils/aoml.ts +++ b/webAO/utils/aoml.ts @@ -1,7 +1,7 @@ import request from "../services/request" interface Aoml { - [key: string]: any | number, + [key: string]: string | number, name: string; start: string; end: string; -- cgit From 08916f6d4eb8db40e6e54f78c744071f3b5298d7 Mon Sep 17 00:00:00 2001 From: "caleb.mabry.15@cnu.edu" Date: Thu, 24 Mar 2022 00:42:24 -0400 Subject: Adding typescript support and unit tests --- webAO/utils/__tests__/tryUrls.test.ts | 31 +++++++++++++++++++++++++++++++ webAO/utils/tryUrls.js | 20 -------------------- webAO/utils/tryUrls.ts | 20 ++++++++++++++++++++ 3 files changed, 51 insertions(+), 20 deletions(-) create mode 100644 webAO/utils/__tests__/tryUrls.test.ts delete mode 100644 webAO/utils/tryUrls.js create mode 100644 webAO/utils/tryUrls.ts diff --git a/webAO/utils/__tests__/tryUrls.test.ts b/webAO/utils/__tests__/tryUrls.test.ts new file mode 100644 index 0000000..444664e --- /dev/null +++ b/webAO/utils/__tests__/tryUrls.test.ts @@ -0,0 +1,31 @@ +import fileExists from '../fileExists' +import tryUrls from '../tryUrls'; +import transparentPng from '../../constants/transparentPng' +jest.mock('../fileExists') + +const mockFileExists = fileExists as jest.MockedFunction; + +describe('tryUrls', () => { + it('Should try multiple file extensions', async () => { + const url = "localhost/stoneddiscord/assets" + mockFileExists + .mockReturnValueOnce(Promise.resolve(false)) + .mockReturnValueOnce(Promise.resolve(false)) + .mockReturnValueOnce(Promise.resolve(false)) + .mockReturnValueOnce(Promise.resolve(true)) + const actual = await tryUrls(url) + const expected = 'localhost/stoneddiscord/assets.apng' + expect(actual).toBe(expected); + }); + + it('Should return a transparent png if it cant find any assets', async () => { + const url = "localhost/stoneddiscord/assets" + mockFileExists + .mockReturnValue(Promise.resolve(false)) + const actual = await tryUrls(url) + const expected = transparentPng + expect(actual).toBe(expected); + }); +}) + + diff --git a/webAO/utils/tryUrls.js b/webAO/utils/tryUrls.js deleted file mode 100644 index db07ec7..0000000 --- a/webAO/utils/tryUrls.js +++ /dev/null @@ -1,20 +0,0 @@ -import fileExists from './fileExists' -import transparentPng from '../constants/transparentPng' -const urlExtensionsToTry = [ - '.png', - '.gif', - '.webp', - '.apng' -] -const tryUrls = async (url) => { - for (let i = 0; i < urlExtensionsToTry.length; i++) { - const extension = urlExtensionsToTry[i] - const fullFileUrl = url + extension - const exists = await fileExists(fullFileUrl); - if (exists) { - return fullFileUrl - } - } - return transparentPng -} -export default tryUrls \ No newline at end of file diff --git a/webAO/utils/tryUrls.ts b/webAO/utils/tryUrls.ts new file mode 100644 index 0000000..14ef885 --- /dev/null +++ b/webAO/utils/tryUrls.ts @@ -0,0 +1,20 @@ +import fileExists from './fileExists' +import transparentPng from '../constants/transparentPng' +const urlExtensionsToTry = [ + '.png', + '.gif', + '.webp', + '.apng' +] +const tryUrls = async (url: string) => { + for (let i = 0; i < urlExtensionsToTry.length; i++) { + const extension = urlExtensionsToTry[i] + const fullFileUrl = url + extension + const exists = await fileExists(fullFileUrl); + if (exists) { + return fullFileUrl + } + } + return transparentPng +} +export default tryUrls \ No newline at end of file -- cgit From 502a472ea297e3c170dd74c1a4542f492cf2ea3d Mon Sep 17 00:00:00 2001 From: Caleb Mabry Date: Fri, 25 Mar 2022 00:15:36 -0400 Subject: Added support for saving chatlog --- webAO/client.js | 45 ++++++++++++++++++++++++++++++++++++++---- webAO/services/downloadFile.ts | 10 ++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 webAO/services/downloadFile.ts diff --git a/webAO/client.js b/webAO/client.js index 6a2947c..10153f6 100644 --- a/webAO/client.js +++ b/webAO/client.js @@ -29,7 +29,7 @@ import queryParser from './utils/queryParser.js'; import getAnimLength from './utils/getAnimLength.js'; import getResources from './utils/getResources.js'; import transparentPng from './constants/transparentPng'; - +import downloadFile from './services/downloadFile' const version = process.env.npm_package_version; let client; @@ -559,13 +559,51 @@ 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 + document.getElementById('client_inputbox').value = ''; + } + /** * Handles an in-character chat message. * @param {*} args packet arguments */ handleMS(args) { + const msMessage = args[5] + const commands = { + '/save_chatlog': this.saveChatlogHandle + } + const commandsMap = new Map(Object.entries(commands)) + + if (msMessage && commandsMap.has(msMessage.toLowerCase())) { + try { + commandsMap.get(msMessage.toLowerCase())() + } catch (e) { + // Command Not Recognized + } + } // TODO: this if-statement might be a bug. - if (args[4] !== viewport.chatmsg.content) { + else if (args[4] !== viewport.chatmsg.content) { document.getElementById('client_inner_chat').innerHTML = ''; const char_id = Number(args[9]); @@ -699,8 +737,7 @@ class Client extends EventEmitter { if (chatmsg.charid === this.charID) { resetICParams(); } - - viewport.say(chatmsg); // no await + viewport.say(chatmsg); // no await } } } diff --git a/webAO/services/downloadFile.ts b/webAO/services/downloadFile.ts new file mode 100644 index 0000000..3774e9b --- /dev/null +++ b/webAO/services/downloadFile.ts @@ -0,0 +1,10 @@ +const downloadFile = (content: string, filename: string) => { + + const a = document.createElement('a'); + const file = new Blob([content], {type: 'text'}); + + a.href= URL.createObjectURL(file); + a.download = filename; + a.click(); +} +export default downloadFile \ No newline at end of file -- cgit From f7d7e96f3d36bc2bd21d58027a8610a75933eb6c Mon Sep 17 00:00:00 2001 From: Caleb Mabry Date: Fri, 25 Mar 2022 13:14:41 -0400 Subject: added unit tests --- webAO/services/__tests__/downloadFile.test.ts | 27 +++++++++++++++++++++++++++ webAO/services/downloadFile.ts | 2 -- 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 webAO/services/__tests__/downloadFile.test.ts diff --git a/webAO/services/__tests__/downloadFile.test.ts b/webAO/services/__tests__/downloadFile.test.ts new file mode 100644 index 0000000..b217b2c --- /dev/null +++ b/webAO/services/__tests__/downloadFile.test.ts @@ -0,0 +1,27 @@ +import downloadFile from '../downloadFile' +jest + .useFakeTimers() + .setSystemTime(new Date('2020-01-01').getTime()); + + global.URL.createObjectURL = jest.fn(); +(window as any).global.Blob = function (content, options){return ({content, options})} + +describe('downloadFile', () => { + it('Creates an tag', () => { + const createElementSpy = jest.spyOn(document, 'createElement'); + downloadFile('hi', 'filename') + expect(createElementSpy).toBeCalled() + }) + it('Creates the blob with the correct data', () => { + const data = 'writingtestsishard' + global.URL.createObjectURL = jest.fn(() => data); + downloadFile(data, 'filename') + const expected = { + content: [data], + options: { + type: "text" + } + } + expect(global.URL.createObjectURL).toBeCalledWith(expected) + }) +}) \ No newline at end of file diff --git a/webAO/services/downloadFile.ts b/webAO/services/downloadFile.ts index 3774e9b..058075f 100644 --- a/webAO/services/downloadFile.ts +++ b/webAO/services/downloadFile.ts @@ -1,8 +1,6 @@ const downloadFile = (content: string, filename: string) => { - const a = document.createElement('a'); const file = new Blob([content], {type: 'text'}); - a.href= URL.createObjectURL(file); a.download = filename; a.click(); -- cgit From 8406b6f1fb6ce6e61dab3e39f1a5751c49e6d184 Mon Sep 17 00:00:00 2001 From: Caleb Mabry Date: Fri, 25 Mar 2022 13:27:17 -0400 Subject: Updated tests --- webAO/client/__tests__/setEmote.test.js | 50 ++++++++++++++++----------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/webAO/client/__tests__/setEmote.test.js b/webAO/client/__tests__/setEmote.test.js index d81c2cc..1db13c9 100644 --- a/webAO/client/__tests__/setEmote.test.js +++ b/webAO/client/__tests__/setEmote.test.js @@ -1,10 +1,10 @@ import setEmote from '../setEmote'; import Client from '../../client'; -import fileExistsSync from '../../utils/fileExistsSync'; +import fileExists from '../../utils/fileExists'; import transparentPng from '../../constants/transparentPng'; jest.mock('../../client'); -jest.mock('../../utils/fileExistsSync'); +jest.mock('../../utils/fileExists'); describe('setEmote', () => { const AO_HOST = ''; @@ -17,98 +17,98 @@ describe('setEmote', () => { const client = new Client('127.0.0.1'); const firstExtension = '.gif'; - test('Should have a client_def_char_img with a valid source', () => { - fileExistsSync.mockReturnValue(true); + test('Should have a client_def_char_img with a valid source', async () => { + fileExists.mockReturnValue(true); document.body.innerHTML = ` `; - setEmote(AO_HOST, client, 'salanto', 'coding', '(a)', 0, 'def'); + await setEmote(AO_HOST, client, 'salanto', 'coding', '(a)', 0, 'def'); const expected = `http://localhost/characters/salanto/(a)coding${firstExtension}`; expect(document.getElementById('client_def_char_img').src).toEqual(expected); }); - test('Should have a client_pro_char_img to have a valid src', () => { + test('Should have a client_pro_char_img to have a valid src', async () => { document.body.innerHTML = ` `; - setEmote(AO_HOST, client, 'salanto', 'coding', '(a)', 0, 'pro'); + await setEmote(AO_HOST, client, 'salanto', 'coding', '(a)', 0, 'pro'); const expected = `http://localhost/characters/salanto/(a)coding${firstExtension}`; expect(document.getElementById('client_pro_char_img').src).toEqual(expected); }); - test('Should have a client_wit_char_img', () => { + test('Should have a client_wit_char_img', async () => { document.body.innerHTML = ` `; - setEmote(AO_HOST, client, 'salanto', 'coding', '(a)', 0, 'wit'); + await setEmote(AO_HOST, client, 'salanto', 'coding', '(a)', 0, 'wit'); const expected = `http://localhost/characters/salanto/(a)coding${firstExtension}`; expect(document.getElementById('client_wit_char_img').src).toEqual(expected); }); - test('Should have a client_def_pair_img', () => { + test('Should have a client_def_pair_img', async () => { document.body.innerHTML = ` `; - setEmote(AO_HOST, client, 'salanto', 'coding', '(a)', 1, 'def'); + await setEmote(AO_HOST, client, 'salanto', 'coding', '(a)', 1, 'def'); const expected = `http://localhost/characters/salanto/(a)coding${firstExtension}`; expect(document.getElementById('client_def_pair_img').src).toEqual(expected); }); - test('Should have a client_pro_pair_img', () => { + test('Should have a client_pro_pair_img', async () => { document.body.innerHTML = ` `; - setEmote(AO_HOST, client, 'salanto', 'coding', '(a)', 1, 'pro'); + await setEmote(AO_HOST, client, 'salanto', 'coding', '(a)', 1, 'pro'); const expected = `http://localhost/characters/salanto/(a)coding${firstExtension}`; expect(document.getElementById('client_pro_pair_img').src).toEqual(expected); }); - test('Should have a client_wit_pair_img', () => { + test('Should have a client_wit_pair_img', async () => { document.body.innerHTML = ` `; - setEmote(AO_HOST, client, 'salanto', 'coding', '(a)', 1, 'wit'); + await setEmote(AO_HOST, client, 'salanto', 'coding', '(a)', 1, 'wit'); const expected = `http://localhost/characters/salanto/(a)coding${firstExtension}`; expect(document.getElementById('client_wit_pair_img').src).toEqual(expected); }); - test('Should have a client_char_img', () => { + test('Should have a client_char_img', async () => { document.body.innerHTML = ` `; - setEmote(AO_HOST, client, 'salanto', 'coding', '(a)', 0, 'notvalid'); + await setEmote(AO_HOST, client, 'salanto', 'coding', '(a)', 0, 'notvalid'); const expected = `http://localhost/characters/salanto/(a)coding${firstExtension}`; expect(document.getElementById('client_char_img').src).toEqual(expected); }); - test('Should have a client_pair_img', () => { + test('Should have a client_pair_img', async () => { document.body.innerHTML = ` `; - setEmote(AO_HOST, client, 'salanto', 'coding', '(a)', 1, 'notvalid'); + await setEmote(AO_HOST, client, 'salanto', 'coding', '(a)', 1, 'notvalid'); const expected = `http://localhost/characters/salanto/(a)coding${firstExtension}`; expect(document.getElementById('client_pair_img').src).toEqual(expected); }); - test('Should handle .png urls differently', () => { - fileExistsSync.mockReturnValueOnce(false); + test('Should handle .png urls differently', async () => { + fileExists.mockReturnValueOnce(false); document.body.innerHTML = ` `; - setEmote(AO_HOST, client, 'salanto', 'coding', 'prefixNotValid', 1, 'notvalid'); + await setEmote(AO_HOST, client, 'salanto', 'coding', 'prefixNotValid', 1, 'notvalid'); const expected = 'http://localhost/characters/salanto/coding.png'; expect(document.getElementById('client_pair_img').src).toEqual(expected); }); - test('Should replace character if new character responds', () => { - fileExistsSync.mockReturnValue(false); + test('Should replace character if new character responds', async () => { + fileExists.mockReturnValue(false); document.body.innerHTML = ` `; - setEmote(AO_HOST, client, 'salanto', 'coding', 'prefixNotValid', 1, 'notvalid'); + await setEmote(AO_HOST, client, 'salanto', 'coding', 'prefixNotValid', 1, 'notvalid'); const expected = transparentPng; expect(document.getElementById('client_pair_img').src).toEqual(expected); }); -- cgit From 3b697cd8dba78e840e9d85380bf4e8dab0d7c672 Mon Sep 17 00:00:00 2001 From: Caleb Mabry Date: Fri, 25 Mar 2022 13:56:27 -0400 Subject: Moving command check to OOC --- webAO/client.js | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/webAO/client.js b/webAO/client.js index 10153f6..9b11a49 100644 --- a/webAO/client.js +++ b/webAO/client.js @@ -232,7 +232,21 @@ class Client extends EventEmitter { setCookie('OOC_name', document.getElementById('OOC_name').value); const oocName = `${escapeChat(encodeChat(document.getElementById('OOC_name').value))}`; const oocMessage = `${escapeChat(encodeChat(message))}`; - this.sendServer(`CT#${oocName}#${oocMessage}#%`); + + const commands = { + '/save_chatlog': this.saveChatlogHandle + } + const commandsMap = new Map(Object.entries(commands)) + + if (oocMessage && commandsMap.has(oocMessage.toLowerCase())) { + try { + commandsMap.get(oocMessage.toLowerCase())() + } catch (e) { + // Command Not Recognized + } + } else { + this.sendServer(`CT#${oocName}#${oocMessage}#%`); + } } /** @@ -589,21 +603,9 @@ class Client extends EventEmitter { * @param {*} args packet arguments */ handleMS(args) { - const msMessage = args[5] - const commands = { - '/save_chatlog': this.saveChatlogHandle - } - const commandsMap = new Map(Object.entries(commands)) - if (msMessage && commandsMap.has(msMessage.toLowerCase())) { - try { - commandsMap.get(msMessage.toLowerCase())() - } catch (e) { - // Command Not Recognized - } - } // TODO: this if-statement might be a bug. - else if (args[4] !== viewport.chatmsg.content) { + if (args[4] !== viewport.chatmsg.content) { document.getElementById('client_inner_chat').innerHTML = ''; const char_id = Number(args[9]); @@ -754,6 +756,7 @@ class Client extends EventEmitter { oocLog.scrollTop = oocLog.scrollHeight; } } + } /** -- cgit From 6312de13da0edce2c4e460adba732d6a8953a2cd Mon Sep 17 00:00:00 2001 From: Caleb Mabry Date: Fri, 25 Mar 2022 20:06:40 -0400 Subject: I've been to hell and back --- webAO/client.js | 85 +++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 55 insertions(+), 30 deletions(-) diff --git a/webAO/client.js b/webAO/client.js index 06defa2..584b95c 100644 --- a/webAO/client.js +++ b/webAO/client.js @@ -1705,7 +1705,7 @@ class Viewport { this.testimonyTimer = 0; this.shoutTimer = 0; - this.textTimer = 0; + this.tickTimer = 0; this._animating = false; } @@ -1934,8 +1934,11 @@ class Viewport { this.chatmsg = chatmsg; this.textnow = ''; this.sfxplayed = 0; - this.textTimer = 0; + this.tickTimer = 0; this._animating = true; + this.startFirstTickCheck = true + this.startSecondTickCheck = false + this.startThirdTickCheck = false let charLayers = document.getElementById('client_char'); let pairLayers = document.getElementById('client_pair_char'); @@ -2086,6 +2089,36 @@ class Viewport { this.tick(); } + handleTextTick(charLayers) { + const chatBox = document.getElementById('client_chat'); + const waitingBox = document.getElementById('client_chatwaiting'); + const chatBoxInner = document.getElementById('client_inner_chat'); + const charName = this.chatmsg.name.toLowerCase(); + const charEmote = this.chatmsg.sprite.toLowerCase(); + + + if (this.chatmsg.content.charAt(this.textnow.length) !== ' ') { + this.blipChannels[this.currentBlipChannel].play(); + this.currentBlipChannel++; + this.currentBlipChannel %= this.blipChannels.length; + } + this.textnow = this.chatmsg.content.substring(0, this.textnow.length + 1); + const characterElement = this.chatmsg.parsed[this.textnow.length - 1] + if (characterElement) { + chatBoxInner.appendChild(this.chatmsg.parsed[this.textnow.length - 1]); + } + + // scroll to bottom + chatBox.scrollTop = chatBox.scrollHeight; + + if (this.textnow === this.chatmsg.content) { + this._animating = false; + setEmote(AO_HOST, this, charName, charEmote, '(a)', false, this.chatmsg.side); + charLayers.style.opacity = 1; + waitingBox.style.opacity = 1; + clearTimeout(this.updater); + } + } /** * Updates the chatbox based on the given text. * @@ -2124,9 +2157,8 @@ class Viewport { const waitingBox = document.getElementById('client_chatwaiting'); const eviBox = document.getElementById('client_evi'); const shoutSprite = document.getElementById('client_shout'); - const chatBoxInner = document.getElementById('client_inner_chat'); - const chatBox = document.getElementById('client_chat'); const effectlayer = document.getElementById('client_fg'); + const chatBoxInner = document.getElementById('client_inner_chat'); let charLayers = document.getElementById('client_char'); let pairLayers = document.getElementById('client_pair_char'); @@ -2142,7 +2174,9 @@ class Viewport { const pairEmote = this.chatmsg.other_emote.toLowerCase(); // TODO: preanims sometimes play when they're not supposed to - if (this.textTimer >= this.shoutTimer && this.chatmsg.startpreanim) { + const isShoutOver = this.tickTimer >= this.shoutTimer + const isShoutAndPreanimOver = this.tickTimer >= this.shoutTimer + this.chatmsg.preanimdelay + if (isShoutOver && this.startFirstTickCheck) { // Effect stuff if (this.chatmsg.screenshake === 1) { // Shake screen @@ -2169,11 +2203,23 @@ class Viewport { } else { pairLayers.style.opacity = 0; } + // Done with first check, move to second + this.startFirstTickCheck = false + this.startSecondTickCheck = true this.chatmsg.startpreanim = false; this.chatmsg.startspeaking = true; - } else if (this.textTimer >= this.shoutTimer + this.chatmsg.preanimdelay && !this.chatmsg.startpreanim) { + } + const hasNonInterruptingPreAnim = this.chatmsg.noninterrupting_preanim === 1 + if (this.textnow !== this.chatmsg.content && hasNonInterruptingPreAnim) { + const chatContainerBox = document.getElementById('client_chatcontainer'); + chatContainerBox.style.opacity = 1; + this.handleTextTick(charLayers) + + }else if (isShoutAndPreanimOver && this.startSecondTickCheck) { if (this.chatmsg.startspeaking) { + this.chatmsg.startspeaking = false; + // Evidence Bullshit if (this.chatmsg.evidence > 0) { // Prepare evidence @@ -2203,7 +2249,6 @@ class Viewport { chatBoxInner.className = `text_${this.colors[this.chatmsg.color]}`; - this.chatmsg.startspeaking = false; if (this.chatmsg.preanimdelay === 0) { shoutSprite.style.opacity = 0; @@ -2228,37 +2273,17 @@ class Viewport { clearTimeout(this.updater); } } else if (this.textnow !== this.chatmsg.content) { - if (this.chatmsg.content.charAt(this.textnow.length) !== ' ') { - this.blipChannels[this.currentBlipChannel].play(); - this.currentBlipChannel++; - this.currentBlipChannel %= this.blipChannels.length; - } - this.textnow = this.chatmsg.content.substring(0, this.textnow.length + 1); - const characterElement = this.chatmsg.parsed[this.textnow.length - 1] - if (characterElement) { - chatBoxInner.appendChild(this.chatmsg.parsed[this.textnow.length - 1]); - } - - // scroll to bottom - chatBox.scrollTop = chatBox.scrollHeight; - - if (this.textnow === this.chatmsg.content) { - this._animating = false; - setEmote(AO_HOST, this, charName, charEmote, '(a)', false, this.chatmsg.side); - charLayers.style.opacity = 1; - waitingBox.style.opacity = 1; - clearTimeout(this.updater); - } + this.handleTextTick(charLayers) } } - if (!this.sfxplayed && this.chatmsg.snddelay + this.shoutTimer >= this.textTimer) { + if (!this.sfxplayed && this.chatmsg.snddelay + this.shoutTimer >= this.tickTimer) { this.sfxplayed = 1; if (this.chatmsg.sound !== '0' && this.chatmsg.sound !== '1' && this.chatmsg.sound !== '' && this.chatmsg.sound !== undefined && (this.chatmsg.type == 1 || this.chatmsg.type == 2 || this.chatmsg.type == 6)) { this.playSFX(`${AO_HOST}sounds/general/${encodeURI(this.chatmsg.sound.toLowerCase())}.opus`, this.chatmsg.looping_sfx); } } - this.textTimer += UPDATE_INTERVAL; + this.tickTimer += UPDATE_INTERVAL; } } -- cgit From e1cb9e4b204f57d6313a359566b908e90611ab1a Mon Sep 17 00:00:00 2001 From: stonedDiscord Date: Sat, 26 Mar 2022 16:34:33 +0100 Subject: hello typescript my old friend --- tsconfig.json | 3 +- webAO/client.js | 3191 ------------------------------------------- webAO/client.ts | 3283 +++++++++++++++++++++++++++++++++++++++++++++ webAO/dom/changeVolume.js | 8 + webpack.config.js | 2 +- 5 files changed, 3294 insertions(+), 3193 deletions(-) delete mode 100644 webAO/client.js create mode 100644 webAO/client.ts diff --git a/tsconfig.json b/tsconfig.json index 85f6983..16f950d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "outDir": "./dist", "allowJs": true, "target": "es5", - "strict": true, + "lib": ["DOM","DOM.Iterable"], + "strict": false, "strictNullChecks": false, //document.getElementBy "downlevelIteration": true }, diff --git a/webAO/client.js b/webAO/client.js deleted file mode 100644 index b8dbb6b..0000000 --- a/webAO/client.js +++ /dev/null @@ -1,3191 +0,0 @@ -/* - * Glorious webAO - * made by sD, refactored by oldmud0 and Qubrick - * credits to aleks for original idea and source -*/ - -import FingerprintJS from '@fingerprintjs/fingerprintjs'; -import { EventEmitter } from 'events'; -import tryUrls from './utils/tryUrls' -import { - escapeChat, encodeChat, prepChat, safeTags, -} from './encoding'; -import mlConfig from './utils/aoml'; -// Load some defaults for the background and evidence dropdowns -import vanilla_character_arr from './constants/characters.js'; -import vanilla_music_arr from './constants/music.js'; -import vanilla_background_arr from './constants/backgrounds.js'; -import vanilla_evidence_arr from './constants/evidence.js'; - -import chatbox_arr from './styles/chatbox/chatboxes.js'; -import iniParse from './iniParse'; -import getCookie from './utils/getCookie.js'; -import setCookie from './utils/setCookie.js'; -import { request } from './services/request.js'; -import { changeShoutVolume, changeSFXVolume } from './dom/changeVolume.js'; -import setEmote from './client/setEmote.js'; -import fileExists from './utils/fileExists.js'; -import queryParser from './utils/queryParser.js'; -import getAnimLength from './utils/getAnimLength.js'; -import getResources from './utils/getResources.js'; -import transparentPng from './constants/transparentPng'; -import downloadFile from './services/downloadFile' -const version = process.env.npm_package_version; - -let client; -let viewport; -// Get the arguments from the URL bar -let { - ip: serverIP, mode, asset, theme, -} = queryParser(); -// Unless there is an asset URL specified, use the wasabi one -const DEFAULT_HOST = 'http://attorneyoffline.de/base/'; -let AO_HOST = asset || DEFAULT_HOST; -const THEME = theme || 'default'; - -const attorneyMarkdown = mlConfig(AO_HOST) - -const UPDATE_INTERVAL = 60; - -/** - * Toggles AO1-style loading using paginated music packets for mobile platforms. - * The old loading uses more smaller packets instead of a single big one, - * which caused problems on low-memory devices in the past. - */ -let oldLoading = false; - -// presettings -let selectedMenu = 1; -let selectedShout = 0; - -let extrafeatures = []; - -let hdid; - -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(); -fpPromise - .then((fp) => fp.get()) - .then((result) => { - hdid = result.visitorId; - client = new Client(serverIP); - viewport = new Viewport(); - - isLowMemory(); - client.loadResources(); - }); - -let lastICMessageTime = new Date(0); - -class Client extends EventEmitter { - constructor(address) { - super(); - if (mode !== 'replay') { - this.serv = new WebSocket(`ws://${address}`); - // Assign the websocket events - this.serv.addEventListener('open', this.emit.bind(this, 'open')); - this.serv.addEventListener('close', this.emit.bind(this, 'close')); - this.serv.addEventListener('message', this.emit.bind(this, 'message')); - this.serv.addEventListener('error', this.emit.bind(this, 'error')); - } else { - this.joinServer(); - } - - this.on('open', this.onOpen.bind(this)); - this.on('close', this.onClose.bind(this)); - this.on('message', this.onMessage.bind(this)); - this.on('error', this.onError.bind(this)); - - // Preset some of the variables - - this.hp = [0, 0]; - - this.playerID = 1; - this.charID = -1; - this.char_list_length = 0; - this.evidence_list_length = 0; - this.music_list_length = 0; - this.testimonyID = 0; - - this.chars = []; - this.emotes = []; - this.evidences = []; - this.areas = []; - this.musics = []; - - this.musics_time = false; - - this.callwords = []; - - this.banned = false; - - this.resources = getResources(AO_HOST, THEME); - - this.selectedEmote = -1; - this.selectedEvidence = 0; - - this.checkUpdater = null; - - /** - * Assign handlers for all commands - * If you implement a new command, you need to add it here - */ - this.on('MS', this.handleMS.bind(this)); - this.on('CT', this.handleCT.bind(this)); - this.on('MC', this.handleMC.bind(this)); - this.on('RMC', this.handleRMC.bind(this)); - this.on('CI', this.handleCI.bind(this)); - this.on('SC', this.handleSC.bind(this)); - this.on('EI', this.handleEI.bind(this)); - this.on('FL', this.handleFL.bind(this)); - this.on('LE', this.handleLE.bind(this)); - this.on('EM', this.handleEM.bind(this)); - this.on('FM', this.handleFM.bind(this)); - this.on('FA', this.handleFA.bind(this)); - this.on('SM', this.handleSM.bind(this)); - this.on('MM', this.handleMM.bind(this)); - this.on('BD', this.handleBD.bind(this)); - this.on('BB', this.handleBB.bind(this)); - this.on('KB', this.handleKB.bind(this)); - this.on('KK', this.handleKK.bind(this)); - this.on('DONE', this.handleDONE.bind(this)); - this.on('BN', this.handleBN.bind(this)); - this.on('HP', this.handleHP.bind(this)); - this.on('RT', this.handleRT.bind(this)); - this.on('TI', this.handleTI.bind(this)); - this.on('ZZ', this.handleZZ.bind(this)); - this.on('HI', this.handleHI.bind(this)); - this.on('ID', this.handleID.bind(this)); - this.on('PN', this.handlePN.bind(this)); - this.on('SI', this.handleSI.bind(this)); - this.on('ARUP', this.handleARUP.bind(this)); - this.on('askchaa', this.handleaskchaa.bind(this)); - this.on('CC', this.handleCC.bind(this)); - this.on('RC', this.handleRC.bind(this)); - this.on('RM', this.handleRM.bind(this)); - this.on('RD', this.handleRD.bind(this)); - this.on('CharsCheck', this.handleCharsCheck.bind(this)); - this.on('PV', this.handlePV.bind(this)); - this.on('ASS', this.handleASS.bind(this)); - this.on('CHECK', () => { }); - this.on('CH', () => { }); - - this._lastTimeICReceived = new Date(0); - } - - /** - * Gets the current player's character. - */ - get character() { - return this.chars[this.charID]; - } - - /** - * Gets the player's currently selected emote. - */ - get emote() { - return this.emotes[this.selectedEmote]; - } - - /** - * Gets the current evidence ID unless the player doesn't want to present any evidence - */ - get evidence() { - return (document.getElementById('button_present').classList.contains('dark')) ? this.selectedEvidence : 0; - } - - /** - * Hook for sending messages to the server - * @param {string} message the message to send - */ - sendServer(message) { - mode === 'replay' ? this.sendSelf(message) : this.serv.send(message); - } - - /** - * Hook for sending messages to the client - * @param {string} message the message to send - */ - handleSelf(message) { - const message_event = new MessageEvent('websocket', { data: message }); - setTimeout(() => this.onMessage(message_event), 1); - } - - /** - * Hook for sending messages to the client - * @param {string} message the message to send - */ - sendSelf(message) { - document.getElementById('client_ooclog').value += `${message}\r\n`; - this.handleSelf(message); - } - - /** - * Sends an out-of-character chat message. - * @param {string} message the message to send - */ - sendOOC(message) { - setCookie('OOC_name', document.getElementById('OOC_name').value); - const oocName = `${escapeChat(encodeChat(document.getElementById('OOC_name').value))}`; - const oocMessage = `${escapeChat(encodeChat(message))}`; - - const commands = { - '/save_chatlog': this.saveChatlogHandle - } - const commandsMap = new Map(Object.entries(commands)) - - if (oocMessage && commandsMap.has(oocMessage.toLowerCase())) { - try { - commandsMap.get(oocMessage.toLowerCase())() - } catch (e) { - // Command Not Recognized - } - } else { - this.sendServer(`CT#${oocName}#${oocMessage}#%`); - } - } - - /** - * Sends an in-character chat message. - * @param {string} deskmod currently unused - * @param {string} speaking who is speaking - * @param {string} name the name of the current character - * @param {string} silent whether or not it's silent - * @param {string} message the message to be sent - * @param {string} side the name of the side in the background - * @param {string} sfx_name the name of the sound effect - * @param {string} emote_modifier whether or not to zoom - * @param {number} sfx_delay the delay (in milliseconds) to play the sound effect - * @param {string} objection_modifier the number of the shout to play - * @param {string} evidence the filename of evidence to show - * @param {number} flip change to 1 to reverse sprite for position changes - * @param {number} realization screen flash effect - * @param {number} text_color text color - * @param {string} showname custom name to be displayed (optional) - * @param {number} other_charid paired character (optional) - * @param {number} self_offset offset to paired character (optional) - * @param {number} noninterrupting_preanim play the full preanim (optional) - */ - sendIC( - deskmod, - preanim, - name, - emote, - message, - side, - sfx_name, - emote_modifier, - sfx_delay, - objection_modifier, - evidence, - flip, - realization, - text_color, - showname, - other_charid, - self_hoffset, - self_yoffset, - noninterrupting_preanim, - looping_sfx, - screenshake, - frame_screenshake, - frame_realization, - frame_sfx, - additive, - effect, - ) { - let extra_cccc = ''; - let other_emote = ''; - let other_offset = ''; - let extra_27 = ''; - let extra_28 = ''; - - if (extrafeatures.includes('cccc_ic_support')) { - const self_offset = extrafeatures.includes('y_offset') ? `${self_hoffset}${self_yoffset}` : self_hoffset; // HACK: this should be an & but client fucked it up and all the servers adopted it - if (mode === 'replay') { - other_emote = '##'; - other_offset = '#0#0'; - } - extra_cccc = `${showname}#${other_charid}${other_emote}#${self_offset}${other_offset}#${noninterrupting_preanim}#`; - - if (extrafeatures.includes('looping_sfx')) { - extra_27 = `${looping_sfx}#${screenshake}#${frame_screenshake}#${frame_realization}#${frame_sfx}#`; - if (extrafeatures.includes('effects')) { - extra_28 = `${additive}#${effect}#`; - } - } - } - - const serverMessage = `MS#${deskmod}#${preanim}#${name}#${emote}` - + `#${escapeChat(encodeChat(message))}#${side}#${sfx_name}#${emote_modifier}` - + `#${this.charID}#${sfx_delay}#${objection_modifier}#${evidence}#${flip}#${realization}#${text_color}#${extra_cccc}${extra_27}${extra_28}%`; - - this.sendServer(serverMessage); - if (mode === 'replay') { - document.getElementById('client_ooclog').value += `wait#${document.getElementById('client_replaytimer').value}#%\r\n`; - } - } - - /** - * Sends add evidence command. - * @param {string} evidence name - * @param {string} evidence description - * @param {string} evidence image filename - */ - sendPE(name, desc, img) { - this.sendServer(`PE#${escapeChat(encodeChat(name))}#${escapeChat(encodeChat(desc))}#${img}#%`); - } - - /** - * Sends edit evidence command. - * @param {number} evidence id - * @param {string} evidence name - * @param {string} evidence description - * @param {string} evidence image filename - */ - sendEE(id, name, desc, img) { - this.sendServer(`EE#${id}#${escapeChat(encodeChat(name))}#${escapeChat(encodeChat(desc))}#${img}#%`); - } - - /** - * Sends delete evidence command. - * @param {number} evidence id - */ - sendDE(id) { - this.sendServer(`DE#${id}#%`); - } - - /** - * Sends health point command. - * @param {number} side the position - * @param {number} hp the health point - */ - sendHP(side, hp) { - this.sendServer(`HP#${side}#${hp}#%`); - } - - /** - * Sends call mod command. - * @param {string} message to mod - */ - sendZZ(msg) { - if (extrafeatures.includes('modcall_reason')) { - this.sendServer(`ZZ#${msg}#%`); - } else { - this.sendServer('ZZ#%'); - } - } - - /** - * Sends testimony command. - * @param {string} testimony type - */ - sendRT(testimony) { - if (this.chars[this.charID].side === 'jud') { - this.sendServer(`RT#${testimony}#%`); - } - } - - /** - * Requests to change the music to the specified track. - * @param {string} track the track ID - */ - sendMusicChange(track) { - this.sendServer(`MC#${track}#${this.charID}#%`); - } - - /** - * Begins the handshake process by sending an identifier - * to the server. - */ - joinServer() { - this.sendServer(`HI#${hdid}#%`); - this.sendServer('ID#webAO#webAO#%'); - if (mode !== 'replay') { this.checkUpdater = setInterval(() => this.sendCheck(), 5000); } - } - - /** - * Load game resources and stored settings. - */ - loadResources() { - document.getElementById('client_version').innerText = `version ${version}`; - - // Load background array to select - const background_select = 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 = 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 - document.getElementById('OOC_name').value = getCookie('OOC_name') || `web${parseInt(Math.random() * 100 + 10)}`; - - // Read cookies and set the UI to its values - const cookietheme = getCookie('theme') || 'default'; - - document.querySelector(`#client_themeselect [value="${cookietheme}"]`).selected = true; - reloadTheme(); - - const cookiechatbox = getCookie('chatbox') || 'dynamic'; - - document.querySelector(`#client_chatboxselect [value="${cookiechatbox}"]`).selected = true; - setChatbox(cookiechatbox); - - document.getElementById('client_mvolume').value = getCookie('musicVolume') || 1; - changeMusicVolume(); - document.getElementById('client_sfxaudio').volume = getCookie('sfxVolume') || 1; - changeSFXVolume(); - document.getElementById('client_shoutaudio').volume = getCookie('shoutVolume') || 1; - changeShoutVolume(); - document.getElementById('client_testimonyaudio').volume = getCookie('testimonyVolume') || 1; - changeTestimonyVolume(); - document.getElementById('client_bvolume').value = getCookie('blipVolume') || 1; - changeBlipVolume(); - - document.getElementById('ic_chat_name').value = getCookie('ic_chat_name'); - document.getElementById('showname').checked = getCookie('showname'); - showname_click(); - - document.getElementById('client_callwords').value = getCookie('callwords'); - } - - /** - * Requests to play as a specified character. - * @param {number} character the character ID - */ - sendCharacter(character) { - if (this.chars[character].name) { this.sendServer(`CC#${this.playerID}#${character}#web#%`); } - } - - /** - * Requests to select a music track. - * @param {number?} song the song to be played - */ - sendMusic(song) { - this.sendServer(`MC#${song}#${this.charID}#%`); - } - - /** - * Sends a keepalive packet. - */ - sendCheck() { - this.sendServer(`CH#${this.charID}#%`); - } - - /** - * Triggered when a connection is established to the server. - */ - onOpen(_e) { - client.joinServer(); - } - - /** - * Triggered when the connection to the server closes. - * @param {CloseEvent} e - */ - onClose(e) { - console.error(`The connection was closed: ${e.reason} (${e.code})`); - if (extrafeatures.length == 0 && this.banned === false) { - document.getElementById('client_errortext').textContent = 'Could not connect to the server'; - } - document.getElementById('client_error').style.display = 'flex'; - document.getElementById('client_loading').style.display = 'none'; - document.getElementById('error_id').textContent = e.code; - this.cleanup(); - } - - /** - * Triggered when a packet is received from the server. - * @param {MessageEvent} e - */ - onMessage(e) { - const msg = e.data; - console.debug(`S: ${msg}`); - - const lines = msg.split('%'); - - for (const msg of lines) { - if (msg === '') { break; } - - const args = msg.split('#'); - const header = args[0]; - - if (!this.emit(header, args)) { - console.warn(`Invalid packet header ${header}`); - } - } - } - - /** - * Triggered when an network error occurs. - * @param {ErrorEvent} e - */ - onError(e) { - console.error(`A network error occurred: ${e.reason} (${e.code})`); - document.getElementById('client_error').style.display = 'flex'; - document.getElementById('error_id').textContent = e.code; - this.cleanup(); - } - - /** - * Stop sending keepalives to the server. - */ - cleanup() { - clearInterval(this.checkUpdater); - - // the connection got rekt, get rid of the old musiclist - this.resetMusicList(); - document.getElementById('client_chartable').innerHTML = ''; - } - - /** - * Parse the lines in the OOC and play them - * @param {*} args packet arguments - */ - handleReplay() { - const ooclog = document.getElementById('client_ooclog'); - const rawLog = false; - let rtime = document.getElementById('client_replaytimer').value; - - const clines = ooclog.value.split(/\r?\n/); - if (clines[0]) { - const currentLine = String(clines[0]); - this.handleSelf(currentLine); - ooclog.value = clines.slice(1).join('\r\n'); - if (currentLine.substr(0, 4) === 'wait' && rawLog === false) { - rtime = currentLine.split('#')[1]; - } else if (currentLine.substr(0, 2) !== 'MS') { - rtime = 0; - } - - setTimeout(() => onReplayGo(''), rtime); - } - } - - 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 - document.getElementById('client_inputbox').value = ''; - } - - /** - * Handles an in-character chat message. - * @param {*} args packet arguments - */ - handleMS(args) { - - // TODO: this if-statement might be a bug. - if (args[4] !== viewport.chatmsg.content) { - document.getElementById('client_inner_chat').innerHTML = ''; - - 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; - - try { - msg_nameplate = this.chars[char_id].showname; - msg_blips = this.chars[char_id].blips; - char_chatbox = this.chars[char_id].chat; - char_muted = this.chars[char_id].muted; - - if (this.chars[char_id].name !== char_name) { - console.info(`${this.chars[char_id].name} is iniediting to ${char_name}`); - const chargs = (`${char_name}&` + 'iniediter').split('&'); - this.handleCharacterInfo(chargs, char_id); - } - } catch (e) { - msg_nameplate = args[3]; - msg_blips = 'male'; - char_chatbox = 'default'; - char_muted = false; - console.error("we're still missing some character data"); - } - - 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]), - }; - - 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(''), // HACK: here as well, client is fucked and uses this instead of & - other_offset: args[21].split(''), - other_flip: Number(args[22]), - noninterrupting_preanim: Number(args[23]), - }; - chatmsg = Object.assign(extra_cccc, 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]), - }; - 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); - } - } 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: ['', '', ''], - }; - 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 === this.charID) { - resetICParams(); - } - viewport.say(chatmsg); // no await - } - } - } - - /** - * Handles an out-of-character chat message. - * @param {Array} args packet arguments - */ - handleCT(args) { - if (mode !== 'replay') { - const oocLog = document.getElementById('client_ooclog'); - oocLog.innerHTML += `${prepChat(args[1])}: ${prepChat(args[2])}\r\n`; - if (oocLog.scrollTop > oocLog.scrollHeight - 600) { - oocLog.scrollTop = oocLog.scrollHeight; - } - } - - } - - /** - * Handles a music change to an arbitrary resource. - * @param {Array} args packet arguments - */ - handleMC(args) { - const track = prepChat(args[1]); - let charID = Number(args[2]); - const showname = args[3] || ''; - const looping = Boolean(args[4]); - const channel = Number(args[5]) || 0; - // const fading = Number(args[6]) || 0; // unused in web - - const music = viewport.music[channel]; - let musicname; - music.pause(); - if (track.startsWith('http')) { - music.src = track; - } else { - music.src = `${AO_HOST}sounds/music/${encodeURI(track.toLowerCase())}`; - } - music.loop = looping; - music.play(); - - try { - musicname = this.chars[charID].name; - } catch (e) { - charID = -1; - } - - if (charID >= 0) { - musicname = this.chars[charID].name; - appendICLog(`${musicname} changed music to ${track}`); - } else { - appendICLog(`The music was changed to ${track}`); - } - - document.getElementById('client_trackstatustext').innerText = track; - } - - /** - * Handles a music change to an arbitrary resource, with an offset in seconds. - * @param {Array} args packet arguments - */ - handleRMC(args) { - viewport.music.pause(); - const { music } = viewport; - // Music offset + drift from song loading - music.totime = args[1]; - music.offset = new Date().getTime() / 1000; - music.addEventListener('loadedmetadata', () => { - music.currentTime += parseFloat(music.totime + (new Date().getTime() / 1000 - music.offset)).toFixed(3); - music.play(); - }, false); - } - - /** - * 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, charid) { - if (chargs[0]) { - let cini = {}; - const img = document.getElementById(`demo_${charid}`); - 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.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 = document.getElementById('mute_select'); - mute_select.add(new Option(safeTags(chargs[0]), charid)); - const pair_select = document.getElementById('pair_select'); - pair_select.add(new Option(safeTags(chargs[0]), 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.chat).toLowerCase() : safeTags(cini.options.category).toLowerCase(), - evidence: chargs[3], - icon: img.src, - inifile: cini, - muted: false, - }; - - if (this.chars[charid].blips === '') { this.chars[charid].blips = this.chars[charid].gender; } - - const iniedit_select = document.getElementById('client_ininame'); - iniedit_select.add(new Option(safeTags(chargs[0]))); - } else { - console.warn(`missing charid ${charid}`); - const img = document.getElementById(`demo_${charid}`); - img.style.display = 'none'; - } - } - - /** - * Handles incoming character information, bundling multiple characters - * per packet. - * CI#0#Phoenix&description&&&&#Miles ... - * @param {Array} args packet arguments - */ - handleCI(args) { - // Loop through the 10 characters that were sent - - for (let i = 2; i <= args.length - 2; i++) { - if (i % 2 === 0) { - document.getElementById('client_loadingtext').innerHTML = `Loading Character ${args[1]}/${this.char_list_length}`; - const chargs = args[i].split('&'); - const charid = args[i - 1]; - setTimeout(() => this.handleCharacterInfo(chargs, charid), 500); - } - } - // Request the next pack - this.sendServer(`AN#${(args[1] / 10) + 1}#%`); - } - - /** - * Handles incoming character information, containing all characters - * in one packet. - * @param {Array} args packet arguments - */ - async handleSC(args) { - const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); - - // Add this so people can see characters loading on the screen. - document.getElementById('client_loading').style.display = 'none'; - document.getElementById('client_charselect').style.display = 'block'; - - document.getElementById('client_loadingtext').innerHTML = 'Loading Characters'; - for (let i = 1; i < args.length; i++) { - document.getElementById('client_loadingtext').innerHTML = `Loading Character ${i}/${this.char_list_length}`; - const chargs = args[i].split('&'); - const charid = i - 1; - await sleep(0.1); // TODO: Too many network calls without this. net::ERR_INSUFFICIENT_RESOURCES - this.handleCharacterInfo(chargs, charid); - } - // We're done with the characters, request the music - this.sendServer('RM#%'); - } - - /** - * Handles incoming evidence information, containing only one evidence - * item per packet. - * - * Mostly unimplemented in webAO. - * @param {Array} args packet arguments - */ - handleEI(args) { - document.getElementById('client_loadingtext').innerHTML = `Loading Evidence ${args[1]}/${this.evidence_list_length}`; - this.sendServer('RM#%'); - } - - /** - * Handles incoming evidence list, all evidences at once - * item per packet. - * - * @param {Array} args packet arguments - */ - handleLE(args) { - this.evidences = []; - for (let i = 1; i < args.length - 1; i++) { - const arg = args[i].split('&'); - this.evidences[i - 1] = { - name: prepChat(arg[0]), - desc: prepChat(arg[1]), - filename: safeTags(arg[2]), - icon: `${AO_HOST}evidence/${encodeURI(arg[2].toLowerCase())}`, - }; - } - - const evidence_box = document.getElementById('evidences'); - evidence_box.innerHTML = ''; - for (let i = 1; i <= this.evidences.length; i++) { - evidence_box.innerHTML += `${this.evidences[i - 1].name}`; - } - } - - resetMusicList() { - this.musics = []; - document.getElementById('client_musiclist').innerHTML = ''; - } - - resetAreaList() { - 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 = document.getElementById('bg_select'); - bg_select.innerHTML = ''; - - bg_select.add(new Option('Custom', 0)); - bg_array.forEach((background) => { - 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 = document.getElementById('client_ininame'); - char_select.innerHTML = ''; - - char_array.forEach((character) => { - 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 = document.getElementById('evi_select'); - evi_select.innerHTML = ''; - - evi_array.forEach((evi) => { - 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) { - const audioEndings = ['.wav', '.mp3', '.ogg', '.opus']; - return audioEndings.filter((ending) => trackname.endsWith(ending)).length === 1; - } - - addTrack(trackname) { - const newentry = document.createElement('OPTION'); - newentry.text = trackname; - document.getElementById('client_musiclist').options.add(newentry); - this.musics.push(trackname); - } - - createArea(id, name) { - const thisarea = { - name, - players: 0, - status: 'IDLE', - cm: '', - locked: 'FREE', - }; - - this.areas.push(thisarea); - - // Create area button - const newarea = document.createElement('SPAN'); - newarea.classList = '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(this); - }; - - 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 incoming music information, containing multiple entries - * per packet. - * @param {Array} args packet arguments - */ - handleEM(args) { - document.getElementById('client_loadingtext').innerHTML = 'Loading Music'; - if (args[1] === '0') { - this.resetMusicList(); - this.resetAreaList(); - this.musics_time = false; - } - - for (let i = 2; i < args.length - 1; i++) { - if (i % 2 === 0) { - document.getElementById('client_loadingtext').innerHTML = `Loading Music ${args[1]}/${this.music_list_length}`; - const trackname = safeTags(args[i]); - const trackindex = args[i - 1]; - if (this.musics_time) { - this.addTrack(trackname); - } else if (this.isAudio(trackname)) { - this.musics_time = true; - this.fix_last_area(); - this.addTrack(trackname); - } else { - this.createArea(trackindex, trackname); - } - } - } - - // get the next batch of tracks - this.sendServer(`AM#${(args[1] / 10) + 1}#%`); - } - - /** - * Handles incoming music information, containing all music in one packet. - * @param {Array} args packet arguments - */ - handleSM(args) { - document.getElementById('client_loadingtext').innerHTML = 'Loading Music '; - this.resetMusicList(); - this.resetAreaList(); - - this.musics_time = false; - - for (let i = 1; i < args.length - 1; i++) { - // Check when found the song for the first time - const trackname = safeTags(args[i]); - const trackindex = i - 1; - document.getElementById('client_loadingtext').innerHTML = `Loading Music ${i}/${this.music_list_length}`; - if (this.musics_time) { - this.addTrack(trackname); - } else if (this.isAudio(trackname)) { - this.musics_time = true; - this.fix_last_area(); - this.addTrack(trackname); - } else { - this.createArea(trackindex, trackname); - } - } - - // Music done, carry on - this.sendServer('RD#%'); - } - - /** - * Handles updated music list - * @param {Array} args packet arguments - */ - handleFM(args) { - this.resetMusicList(); - - for (let i = 1; i < args.length - 1; i++) { - // Check when found the song for the first time - this.addTrack(safeTags(args[i])); - } - } - - /** - * Handles updated area list - * @param {Array} args packet arguments - */ - handleFA(args) { - this.resetAreaList(); - - for (let i = 1; i < args.length - 1; i++) { - this.createArea(i - 1, safeTags(args[i])); - } - } - - /** - * Handles the "MusicMode" packet - * @param {Array} args packet arguments - */ - handleMM(_args) { - // It's unused nowadays, as preventing people from changing the music is now serverside - } - - /** - * Handles the kicked packet - * @param {String} type is it a kick or a ban - * @param {String} reason why - */ - handleBans(type, reason) { - document.getElementById('client_error').style.display = 'flex'; - document.getElementById('client_errortext').innerHTML = `${type}:
${reason.replace(/\n/g, '
')}`; - document.getElementsByClassName('client_reconnect')[0].style.display = 'none'; - document.getElementsByClassName('client_reconnect')[1].style.display = 'none'; - } - - /** - * Handles the kicked packet - * @param {Array} args kick reason - */ - handleKK(args) { - this.handleBans('Kicked', safeTags(args[1])); - } - - /** - * Handles the banned packet - * this one is sent when you are kicked off the server - * @param {Array} args ban reason - */ - handleKB(args) { - this.handleBans('Banned', safeTags(args[1])); - this.banned = true; - } - - /** - * Handles the warning packet - * on client this spawns a message box you can't close for 2 seconds - * @param {Array} args ban reason - */ - handleBB(args) { - alert(safeTags(args[1])); - } - - /** - * Handles the banned packet - * this one is sent when you try to reconnect but you're banned - * @param {Array} args ban reason - */ - handleBD(args) { - this.handleBans('Banned', safeTags(args[1])); - this.banned = true; - } - - /** - * Handles the handshake completion packet, meaning the player - * is ready to select a character. - * - * @param {Array} args packet arguments - */ - handleDONE(_args) { - document.getElementById('client_loading').style.display = 'none'; - if (mode === 'watch') { // Spectators don't need to pick a character - document.getElementById('client_charselect').style.display = 'none'; - } - } - - /** - * Handles a background change. - * @param {Array} args packet arguments - */ - - handleBN(args) { - viewport.bgname = safeTags(args[1]); - const bgfolder = viewport.bgFolder; - const bg_index = getIndexFromSelect('bg_select', viewport.bgname); - document.getElementById('bg_select').selectedIndex = bg_index; - updateBackgroundPreview(); - if (bg_index === 0) { - document.getElementById('bg_filename').value = viewport.bgname; - } - - tryUrls(`${AO_HOST}background/${encodeURI(args[1].toLowerCase())}/defenseempty`).then(resp => {document.getElementById('bg_preview').src = resp}); - tryUrls(`${bgfolder}defensedesk`).then((resp) => document.getElementById('client_def_bench').src = resp); - tryUrls(`${bgfolder}stand`).then(resp => {document.getElementById('client_wit_bench').src = resp}); - tryUrls(`${bgfolder}prosecutiondesk`).then(resp => {document.getElementById('client_pro_bench').src = resp}); - tryUrls(`${bgfolder}full`).then(resp => {document.getElementById('client_court').src = resp}); - tryUrls(`${bgfolder}defenseempty`).then(resp => {document.getElementById('client_court_def').src = resp}); - tryUrls(`${bgfolder}transition_def`).then(resp => {document.getElementById('client_court_deft').src = resp}); - tryUrls(`${bgfolder}witnessempty`).then(resp => {document.getElementById('client_court_wit').src = resp}); - tryUrls(`${bgfolder}transition_pro`).then(resp => {document.getElementById('client_court_prot').src = resp}); - tryUrls(`${bgfolder}prosecutorempty`).then(resp => {document.getElementById('client_court_pro').src = resp}); - - if (this.charID === -1) { - viewport.changeBackground('jud'); - } else { - viewport.changeBackground(this.chars[this.charID].side); - } - } - - /** - * Handles a change in the health bars' states. - * @param {Array} args packet arguments - */ - handleHP(args) { - const percent_hp = Number(args[2]) * 10; - let healthbox; - if (args[1] === '1') { - // Def hp - this.hp[0] = args[2]; - healthbox = document.getElementById('client_defense_hp'); - } else { - // Pro hp - this.hp[1] = args[2]; - healthbox = document.getElementById('client_prosecutor_hp'); - } - healthbox.getElementsByClassName('health-bar')[0].style.width = `${percent_hp}%`; - } - - /** - * Handles a testimony states. - * @param {Array} args packet arguments - */ - handleRT(args) { - const judgeid = Number(args[2]); - switch (args[1]) { - case 'testimony1': - this.testimonyID = 1; - break; - case 'testimony2': - // Cross Examination - this.testimonyID = 2; - break; - case 'judgeruling': - this.testimonyID = 3 + judgeid; - break; - default: - console.warn('Invalid testimony'); - } - viewport.initTestimonyUpdater(); - } - - /** - * Handles a timer update - * @param {Array} args packet arguments - */ - handleTI(args) { - const timerid = Number(args[1]); - const type = Number(args[2]); - const timer_value = Number(args[3]); - switch (type) { - case 0: - // - case 1: - document.getElementById(`client_timer${timerid}`).innerText = timer_value; - case 2: - document.getElementById(`client_timer${timerid}`).style.display = ''; - case 3: - document.getElementById(`client_timer${timerid}`).style.display = 'none'; - } - } - - /** - * Handles a modcall - * @param {Array} args packet arguments - */ - handleZZ(args) { - const oocLog = document.getElementById('client_ooclog'); - oocLog.innerHTML += `$Alert: ${prepChat(args[1])}\r\n`; - if (oocLog.scrollTop > oocLog.scrollHeight - 60) { - oocLog.scrollTop = oocLog.scrollHeight; - } - viewport.sfxaudio.pause(); - const oldvolume = viewport.sfxaudio.volume; - viewport.sfxaudio.volume = 1; - viewport.sfxaudio.src = `${AO_HOST}sounds/general/sfx-gallery.opus`; - viewport.sfxaudio.play(); - viewport.sfxaudio.volume = oldvolume; - } - - /** - * Handle the player - * @param {Array} args packet arguments - */ - handleHI(args) { - this.sendSelf(`ID#1#webAO#${version}#%`); - this.sendSelf('FL#fastloading#yellowtext#cccc_ic_support#flipping#looping_sfx#effects#%'); - } - - /** - * Identifies the server and issues a playerID - * @param {Array} args packet arguments - */ - handleID(args) { - this.playerID = Number(args[1]); - this.serverSoftware = args[2].split('&')[0]; - if (this.serverSoftware === 'serverD') { this.serverVersion = args[2].split('&')[1]; } else if (this.serverSoftware === 'webAO') { - oldLoading = false; - this.sendSelf('PN#0#1#%'); - } else { this.serverVersion = args[3]; } - - if (this.serverSoftware === 'serverD' && this.serverVersion === '1377.152') { oldLoading = true; } // bugged version - } - - /** - * Indicates how many users are on this server - * @param {Array} args packet arguments - */ - handlePN(_args) { - this.sendServer('askchaa#%'); - } - - /** - * What? you want a character?? - * @param {Array} args packet arguments - */ - handleCC(args) { - this.sendSelf(`PV#1#CID#${args[2]}#%`); - } - - /** - * What? you want a character list from me?? - * @param {Array} args packet arguments - */ - handleaskchaa(_args) { - this.sendSelf(`SI#${vanilla_character_arr.length}#0#0#%`); - } - - /** - * Handle the change of players in an area. - * @param {Array} args packet arguments - */ - handleARUP(args) { - args = args.slice(1); - for (let i = 0; i < args.length - 2; i++) { - if (this.areas[i]) { // the server sends us ARUP before we even get the area list - const thisarea = document.getElementById(`area${i}`); - switch (Number(args[0])) { - case 0: // playercount - this.areas[i].players = Number(args[i + 1]); - break; - case 1: // status - this.areas[i].status = safeTags(args[i + 1]); - break; - case 2: - this.areas[i].cm = safeTags(args[i + 1]); - break; - case 3: - this.areas[i].locked = safeTags(args[i + 1]); - break; - } - - thisarea.classList = `area-button area-${this.areas[i].status.toLowerCase()}`; - - thisarea.innerText = `${this.areas[i].name} (${this.areas[i].players}) [${this.areas[i].status}]`; - - thisarea.title = `Players: ${this.areas[i].players}\n` - + `Status: ${this.areas[i].status}\n` - + `CM: ${this.areas[i].cm}\n` - + `Area lock: ${this.areas[i].locked}`; - } - } - } - - /** - * With this the server tells us which features it supports - * @param {Array} args list of features - */ - handleFL(args) { - console.info('Server-supported features:'); - console.info(args); - extrafeatures = args; - - if (args.includes('yellowtext')) { - const colorselect = 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); - } - - if (args.includes('cccc_ic_support')) { - document.getElementById('cccc').style.display = ''; - document.getElementById('pairing').style.display = ''; - } - - if (args.includes('flipping')) { - document.getElementById('button_flip').style.display = ''; - } - - if (args.includes('looping_sfx')) { - document.getElementById('button_shake').style.display = ''; - document.getElementById('2.7').style.display = ''; - } - - if (args.includes('effects')) { - document.getElementById('2.8').style.display = ''; - } - - if (args.includes('y_offset')) { - document.getElementById('y_offset').style.display = ''; - } - } - - /** - * Received when the server announces its server info, - * but we use it as a cue to begin retrieving characters. - * @param {Array} args packet arguments - */ - handleSI(args) { - this.char_list_length = Number(args[1]); - this.char_list_length += 1; // some servers count starting from 0 some from 1... - this.evidence_list_length = Number(args[2]); - this.music_list_length = Number(args[3]); - - // create the charselect grid, to be filled by the character loader - document.getElementById('client_chartable').innerHTML = ''; - - for (let i = 0; i < this.char_list_length; i++) { - const demothing = document.createElement('img'); - - demothing.className = 'demothing'; - demothing.id = `demo_${i}`; - const demoonclick = document.createAttribute('onclick'); - demoonclick.value = `pickChar(${i})`; - demothing.setAttributeNode(demoonclick); - - document.getElementById('client_chartable').appendChild(demothing); - } - - // this is determined at the top of this file - if (!oldLoading && extrafeatures.includes('fastloading')) { - this.sendServer('RC#%'); - } else { - this.sendServer('askchar2#%'); - } - } - - /** - * Handles the list of all used and vacant characters. - * @param {Array} args list of all characters represented as a 0 for free or a -1 for taken - */ - handleCharsCheck(args) { - for (let i = 0; i < this.char_list_length; i++) { - const img = document.getElementById(`demo_${i}`); - - if (args[i + 1] === '-1') { img.style.opacity = 0.25; } else if (args[i + 1] === '0') { img.style.opacity = 1; } - } - } - - /** - * Handles the server's assignment of a character for the player to use. - * PV # playerID (unused) # CID # character ID - * @param {Array} args packet arguments - */ - async handlePV(args) { - this.charID = Number(args[3]); - document.getElementById('client_charselect').style.display = 'none'; - - const me = this.chars[this.charID]; - this.selectedEmote = -1; - const { emotes } = this; - const emotesList = document.getElementById('client_emo'); - emotesList.style.display = ''; - emotesList.innerHTML = ''; // Clear emote box - const ini = me.inifile; - me.side = ini.options.side; - updateActionCommands(me.side); - if (ini.emotions.number === 0) { - emotesList.innerHTML = `No emotes available`; - } else { - for (let i = 1; i <= ini.emotions.number; i++) { - try { - const emoteinfo = ini.emotions[i].split('#'); - let esfx; - let esfxd; - try { - esfx = ini.soundn[i] || '0'; - esfxd = Number(ini.soundt[i]) || 0; - } catch (e) { - console.warn('ini sound is completly missing'); - esfx = '0'; - esfxd = 0; - } - // Make sure the asset server is case insensitive, or that everything on it is lowercase - - emotes[i] = { - desc: emoteinfo[0].toLowerCase(), - preanim: emoteinfo[1].toLowerCase(), - emote: emoteinfo[2].toLowerCase(), - zoom: Number(emoteinfo[3]) || 0, - sfx: esfx.toLowerCase(), - sfxdelay: esfxd, - frame_screenshake: '', - frame_realization: '', - frame_sfx: '', - button: `${AO_HOST}characters/${encodeURI(me.name.toLowerCase())}/emotions/button${i}_off.png`, - }; - emotesList.innerHTML - += `${emotes[i].desc}`; - } catch (e) { - console.error(`missing emote ${i}`); - } - } - pickEmotion(1); - } - - if (await fileExists(`${AO_HOST}characters/${encodeURI(me.name.toLowerCase())}/custom.gif`)) { document.getElementById('button_4').style.display = ''; } else { document.getElementById('button_4').style.display = 'none'; } - - const iniedit_select = document.getElementById('client_ininame'); - - // Load iniswaps if there are any - try { - const cswapdata = await request(`${AO_HOST}characters/${encodeURI(me.name.toLowerCase())}/iniswaps.ini`); - const cswap = cswapdata.split('\n'); - - // most iniswaps don't list their original char - if (cswap.length > 0) { - iniedit_select.innerHTML = ''; - - function addIniswap(value) { - iniedit_select.add(new Option(safeTags(value))); - } - - addIniswap(me.name); - cswap.forEach(addIniswap); - } - } catch (err) { - console.info("character doesn't have iniswaps"); - this.fetchCharacterList(); - } - } - - /** - * new asset url!! - * @param {Array} args packet arguments - */ - handleASS(args) { - AO_HOST = args[1]; - } - - /** - * we are asking ourselves what characters there are - * @param {Array} args packet arguments - */ - handleRC(_args) { - this.sendSelf(`SC#${vanilla_character_arr.join('#')}#%`); - } - - /** - * we are asking ourselves what characters there are - * @param {Array} args packet arguments - */ - handleRM(_args) { - this.sendSelf(`SM#${vanilla_music_arr.join('#')}#%`); - } - - /** - * we are asking ourselves what characters there are - * @param {Array} args packet arguments - */ - handleRD(_args) { - this.sendSelf('BN#gs4#%'); - this.sendSelf('DONE#%'); - const ooclog = document.getElementById('client_ooclog'); - ooclog.value = ''; - ooclog.readOnly = false; - - document.getElementById('client_oocinput').style.display = 'none'; - document.getElementById('client_replaycontrols').style.display = 'inline-block'; - } -} - -class Viewport { - constructor() { - this.textnow = ''; - this.chatmsg = { - content: '', - objection: 0, - sound: '', - startpreanim: true, - startspeaking: false, - side: null, - color: 0, - snddelay: 0, - preanimdelay: 0, - }; - - this.shouts = [ - undefined, - 'holdit', - 'objection', - 'takethat', - 'custom', - ]; - - this.colors = [ - 'white', - 'green', - 'red', - 'orange', - 'blue', - 'yellow', - 'pink', - 'cyan', - 'grey', - ]; - - // Allocate multiple blip audio channels to make blips less jittery - const blipSelectors = document.getElementsByClassName('blipSound') - this.blipChannels = [...blipSelectors]; - this.blipChannels.forEach((channel) => channel.volume = 0.5); - this.blipChannels.forEach((channel) => channel.onerror = opusCheck(channel)); - this.currentBlipChannel = 0; - - this.sfxaudio = document.getElementById('client_sfxaudio'); - this.sfxaudio.src = `${AO_HOST}sounds/general/sfx-realization.opus`; - - this.sfxplayed = 0; - - this.shoutaudio = document.getElementById('client_shoutaudio'); - this.shoutaudio.src = `${AO_HOST}misc/default/objection.opus`; - - this.testimonyAudio = document.getElementById('client_testimonyaudio'); - this.testimonyAudio.src = `${AO_HOST}sounds/general/sfx-guilty.opus`; - - const audioChannels = document.getElementsByClassName('audioChannel') - this.music = [...audioChannels]; - this.music.forEach((channel) => channel.volume = 0.5); - this.music.forEach((channel) => channel.onerror = opusCheck(channel)); - - this.updater = null; - this.testimonyUpdater = null; - - this.bgname = 'gs4'; - - this.lastChar = ''; - this.lastEvi = 0; - - this.testimonyTimer = 0; - this.shoutTimer = 0; - this.tickTimer = 0; - - this._animating = false; - } - - /** - * Sets the volume of the music. - * @param {number} volume - */ - set musicVolume(volume) { - this.music.forEach((channel) => channel.volume = volume); - } - - /** - * Returns the path which the background is located in. - */ - get bgFolder() { - return `${AO_HOST}background/${encodeURI(this.bgname.toLowerCase())}/`; - } - - /** - * Play any SFX - * - * @param {string} sfxname - */ - async playSFX(sfxname, looping) { - this.sfxaudio.pause(); - this.sfxaudio.loop = looping; - this.sfxaudio.src = sfxname; - this.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 - */ - async changeBackground(position) { - const bgfolder = viewport.bgFolder; - - const view = document.getElementById('client_fullview'); - - let bench; - if ('def,pro,wit'.includes(position)) { - bench = document.getElementById(`client_${position}_bench`); - } else { - bench = document.getElementById('client_bench_classic'); - } - - let court; - if ('def,pro,wit'.includes(position)) { - court = document.getElementById(`client_court_${position}`); - } else { - court = document.getElementById('client_court_classic'); - } - - const positions = { - def: { - bg: 'defenseempty', - desk: { ao2: 'defensedesk.png', ao1: 'bancodefensa.png' }, - speedLines: 'defense_speedlines.gif', - }, - pro: { - bg: 'prosecutorempty', - desk: { ao2: 'prosecutiondesk.png', ao1: 'bancoacusacion.png' }, - speedLines: 'prosecution_speedlines.gif', - }, - hld: { - bg: 'helperstand', - desk: null, - speedLines: 'defense_speedlines.gif', - }, - hlp: { - bg: 'prohelperstand', - desk: null, - speedLines: 'prosecution_speedlines.gif', - }, - wit: { - bg: 'witnessempty', - desk: { ao2: 'stand.png', ao1: 'estrado.png' }, - speedLines: 'prosecution_speedlines.gif', - }, - jud: { - bg: 'judgestand', - desk: { ao2: 'judgedesk.png', ao1: 'judgedesk.gif' }, - speedLines: 'prosecution_speedlines.gif', - }, - jur: { - bg: 'jurystand', - desk: { ao2: 'jurydesk.png', ao1: 'estrado.png' }, - speedLines: 'defense_speedlines.gif', - }, - sea: { - bg: 'seancestand', - desk: { ao2: 'seancedesk.png', ao1: 'estrado.png' }, - speedLines: 'prosecution_speedlines.gif', - }, - }; - - 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 (viewport.chatmsg.type === 5) { - console.warn('this is a zoom'); - court.src = `${AO_HOST}themes/default/${encodeURI(speedLines)}`; - bench.style.opacity = 0; - } else { - // Set src here - - court.src = await tryUrls(bgfolder + bg) - if (desk) { - const deskFilename = await fileExists(bgfolder + desk.ao2) ? desk.ao2 : desk.ao1; - bench.src = bgfolder + 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 - */ - initTestimonyUpdater() { - const testimonyFilenames = { - 1: 'witnesstestimony', - 2: 'crossexamination', - 3: 'notguilty', - 4: 'guilty', - }; - - const testimony = testimonyFilenames[client.testimonyID]; - if (!testimony) { - console.warn(`Invalid testimony ID ${client.testimonyID}`); - return; - } - - this.testimonyAudio.src = client.resources[testimony].sfx; - this.testimonyAudio.play(); - - const testimonyOverlay = document.getElementById('client_testimony'); - testimonyOverlay.src = client.resources[testimony].src; - testimonyOverlay.style.opacity = 1; - - this.testimonyTimer = 0; - this.testimonyUpdater = setTimeout(() => this.updateTestimony(), UPDATE_INTERVAL); - } - - /** - * Updates the testimony overaly - */ - updateTestimony() { - const testimonyFilenames = { - 1: 'witnesstestimony', - 2: 'crossexamination', - 3: 'notguilty', - 4: 'guilty', - }; - - // Update timer - this.testimonyTimer += UPDATE_INTERVAL; - - const testimony = testimonyFilenames[client.testimonyID]; - const resource = client.resources[testimony]; - if (!resource) { - this.disposeTestimony(); - return; - } - - if (this.testimonyTimer >= resource.duration) { - this.disposeTestimony(); - } else { - this.testimonyUpdater = setTimeout(() => this.updateTestimony(), UPDATE_INTERVAL); - } - } - - /** - * Dispose the testimony overlay - */ - disposeTestimony() { - client.testimonyID = 0; - this.testimonyTimer = 0; - document.getElementById('client_testimony').style.opacity = 0; - clearTimeout(this.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 - */ - async say(chatmsg) { - - this.chatmsg = chatmsg; - this.textnow = ''; - this.sfxplayed = 0; - this.tickTimer = 0; - this._animating = true; - this.startFirstTickCheck = true - this.startSecondTickCheck = false - this.startThirdTickCheck = false - let charLayers = document.getElementById('client_char'); - let pairLayers = document.getElementById('client_pair_char'); - - // stop updater - clearTimeout(this.updater); - - // stop last sfx from looping any longer - this.sfxaudio.loop = false; - - const fg = document.getElementById('client_fg'); - const gamewindow = document.getElementById('client_gamewindow'); - const waitingBox = document.getElementById('client_chatwaiting'); - - // Reset CSS animation - gamewindow.style.animation = ''; - waitingBox.style.opacity = 0; - - const eviBox = document.getElementById('client_evi'); - - if (this.lastEvi !== this.chatmsg.evidence) { - eviBox.style.opacity = '0'; - eviBox.style.height = '0%'; - } - this.lastEvi = this.chatmsg.evidence; - - const validSides = ['def', 'pro', 'wit']; - if (validSides.includes(this.chatmsg.side)) { - charLayers = document.getElementById(`client_${this.chatmsg.side}_char`); - pairLayers = document.getElementById(`client_${this.chatmsg.side}_pair_char`); - } - - const chatContainerBox = document.getElementById('client_chatcontainer'); - const nameBoxInner = document.getElementById('client_inner_name'); - const chatBoxInner = document.getElementById('client_inner_chat'); - - const displayname = (document.getElementById('showname').checked && this.chatmsg.showname !== '') ? this.chatmsg.showname : this.chatmsg.nameplate; - - // Clear out the last message - chatBoxInner.innerText = this.textnow; - nameBoxInner.innerText = displayname; - - if (this.lastChar !== this.chatmsg.name) { - charLayers.style.opacity = 0; - pairLayers.style.opacity = 0; - } - this.lastChar = this.chatmsg.name; - - appendICLog(this.chatmsg.content, this.chatmsg.showname, this.chatmsg.nameplate); - - checkCallword(this.chatmsg.content); - - setEmote(AO_HOST, this, this.chatmsg.name.toLowerCase(), this.chatmsg.sprite, '(a)', false, this.chatmsg.side); - - if (this.chatmsg.other_name) { - setEmote(AO_HOST, this, this.chatmsg.other_name.toLowerCase(), this.chatmsg.other_emote, '(a)', false, this.chatmsg.side); - } - - // gets which shout shall played - const shoutSprite = document.getElementById('client_shout'); - const shout = this.shouts[this.chatmsg.objection]; - if (shout) { - // Hide message box - chatContainerBox.style.opacity = 0; - if (this.chatmsg.objection === 4) { - shoutSprite.src = `${AO_HOST}characters/${encodeURI(this.chatmsg.name.toLowerCase())}/custom.gif`; - } else { - shoutSprite.src = client.resources[shout].src; - shoutSprite.style.animation = 'bubble 700ms steps(10, jump-both)'; - } - shoutSprite.style.opacity = 1; - - this.shoutaudio.src = `${AO_HOST}characters/${encodeURI(this.chatmsg.name.toLowerCase())}/${shout}.opus`; - this.shoutaudio.play(); - this.shoutTimer = client.resources[shout].duration; - } else { - this.shoutTimer = 0; - } - - this.chatmsg.startpreanim = true; - let gifLength = 0; - - if (this.chatmsg.type === 1 && this.chatmsg.preanim !== '-') { - chatContainerBox.style.opacity = 0; - gifLength = await getAnimLength(`${AO_HOST}characters/${encodeURI(this.chatmsg.name.toLowerCase())}/${encodeURI(this.chatmsg.preanim)}`); - this.chatmsg.startspeaking = false; - } else { - this.chatmsg.startspeaking = true; - } - this.chatmsg.preanimdelay = parseInt(gifLength); - - this.changeBackground(chatmsg.side); - - setChatbox(chatmsg.chatbox); - resizeChatbox(); - - // Flip the character - charLayers.style.transform = this.chatmsg.flip === 1 ? 'scaleX(-1)' : 'scaleX(1)'; - - // Shift by the horizontal offset - switch (this.chatmsg.side) { - case 'wit': - pairLayers.style.left = `${200 + Number(this.chatmsg.other_offset[0])}%`; - charLayers.style.left = `${200 + Number(this.chatmsg.self_offset[0])}%`; - break; - case 'pro': - pairLayers.style.left = `${400 + Number(this.chatmsg.other_offset[0])}%`; - charLayers.style.left = `${400 + Number(this.chatmsg.self_offset[0])}%`; - break; - default: - pairLayers.style.left = `${Number(this.chatmsg.other_offset[0])}%`; - charLayers.style.left = `${Number(this.chatmsg.self_offset[0])}%`; - break; - } - - // New vertical offsets - pairLayers.style.top = `${Number(this.chatmsg.other_offset[1])}%`; - charLayers.style.top = `${Number(this.chatmsg.self_offset[1])}%`; - - // flip the paired character - pairLayers.style.transform = this.chatmsg.other_flip === 1 ? 'scaleX(-1)' : 'scaleX(1)'; - - this.blipChannels.forEach((channel) => channel.src = `${AO_HOST}sounds/general/sfx-blip${encodeURI(this.chatmsg.blips.toLowerCase())}.opus`); - - // process markup - if (this.chatmsg.content.startsWith('~~')) { - chatBoxInner.style.textAlign = 'center'; - this.chatmsg.content = this.chatmsg.content.substring(2, this.chatmsg.content.length); - } else { - chatBoxInner.style.textAlign = 'inherit'; - } - - // apply effects - fg.style.animation = ''; - const badEffects = ['', '-', 'none']; - if (this.chatmsg.effects[0] && !badEffects.includes(this.chatmsg.effects[0].toLowerCase())) { - const baseEffectUrl = `${AO_HOST}themes/default/effects/`; - fg.src = `${baseEffectUrl}${encodeURI(this.chatmsg.effects[0].toLowerCase())}.webp`; - } else { - - fg.src = transparentPng; - } - - const soundChecks = ['0', '1', '', undefined]; - if (soundChecks.some((check) => this.chatmsg.sound === check)) { - this.chatmsg.sound = this.chatmsg.effects[2]; - } - this.chatmsg.parsed = await attorneyMarkdown.applyMarkdown(chatmsg.content, this.colors[this.chatmsg.color]) - this.tick(); - } - - handleTextTick(charLayers) { - const chatBox = document.getElementById('client_chat'); - const waitingBox = document.getElementById('client_chatwaiting'); - const chatBoxInner = document.getElementById('client_inner_chat'); - const charName = this.chatmsg.name.toLowerCase(); - const charEmote = this.chatmsg.sprite.toLowerCase(); - - - if (this.chatmsg.content.charAt(this.textnow.length) !== ' ') { - this.blipChannels[this.currentBlipChannel].play(); - this.currentBlipChannel++; - this.currentBlipChannel %= this.blipChannels.length; - } - this.textnow = this.chatmsg.content.substring(0, this.textnow.length + 1); - const characterElement = this.chatmsg.parsed[this.textnow.length - 1] - if (characterElement) { - chatBoxInner.appendChild(this.chatmsg.parsed[this.textnow.length - 1]); - } - - // scroll to bottom - chatBox.scrollTop = chatBox.scrollHeight; - - if (this.textnow === this.chatmsg.content) { - this._animating = false; - setEmote(AO_HOST, this, charName, charEmote, '(a)', false, this.chatmsg.side); - charLayers.style.opacity = 1; - waitingBox.style.opacity = 1; - clearTimeout(this.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 `this.chatmsg`! - */ - tick() { - if (this._animating) { - this.updater = setTimeout(() => this.tick(), UPDATE_INTERVAL); - } - - const gamewindow = document.getElementById('client_gamewindow'); - const waitingBox = document.getElementById('client_chatwaiting'); - const eviBox = document.getElementById('client_evi'); - const shoutSprite = document.getElementById('client_shout'); - const effectlayer = document.getElementById('client_fg'); - const chatBoxInner = document.getElementById('client_inner_chat'); - let charLayers = document.getElementById('client_char'); - let pairLayers = document.getElementById('client_pair_char'); - - if ('def,pro,wit'.includes(this.chatmsg.side)) { - charLayers = document.getElementById(`client_${this.chatmsg.side}_char`); - pairLayers = document.getElementById(`client_${this.chatmsg.side}_pair_char`); - } - - const charName = this.chatmsg.name.toLowerCase(); - const charEmote = this.chatmsg.sprite.toLowerCase(); - - const pairName = this.chatmsg.other_name.toLowerCase(); - const pairEmote = this.chatmsg.other_emote.toLowerCase(); - - // TODO: preanims sometimes play when they're not supposed to - const isShoutOver = this.tickTimer >= this.shoutTimer - const isShoutAndPreanimOver = this.tickTimer >= this.shoutTimer + this.chatmsg.preanimdelay - if (isShoutOver && this.startFirstTickCheck) { - // Effect stuff - if (this.chatmsg.screenshake === 1) { - // Shake screen - this.playSFX(`${AO_HOST}sounds/general/sfx-stab.opus`, false); - gamewindow.style.animation = 'shake 0.2s 1'; - } - if (this.chatmsg.flash === 1) { - // Flash screen - this.playSFX(`${AO_HOST}sounds/general/sfx-realization.opus`, false); - effectlayer.style.animation = 'flash 0.4s 1'; - } - - // Pre-animation stuff - if (this.chatmsg.preanimdelay > 0) { - shoutSprite.style.opacity = 0; - shoutSprite.style.animation = ''; - const preanim = this.chatmsg.preanim.toLowerCase(); - setEmote(AO_HOST, this, charName, preanim, '', false, this.chatmsg.side); - charLayers.style.opacity = 1; - } - - if (this.chatmsg.other_name) { - pairLayers.style.opacity = 1; - } else { - pairLayers.style.opacity = 0; - } - // Done with first check, move to second - this.startFirstTickCheck = false - this.startSecondTickCheck = true - - this.chatmsg.startpreanim = false; - this.chatmsg.startspeaking = true; - } - const hasNonInterruptingPreAnim = this.chatmsg.noninterrupting_preanim === 1 - if (this.textnow !== this.chatmsg.content && hasNonInterruptingPreAnim) { - const chatContainerBox = document.getElementById('client_chatcontainer'); - chatContainerBox.style.opacity = 1; - this.handleTextTick(charLayers) - - }else if (isShoutAndPreanimOver && this.startSecondTickCheck) { - if (this.chatmsg.startspeaking) { - this.chatmsg.startspeaking = false; - - // Evidence Bullshit - if (this.chatmsg.evidence > 0) { - // Prepare evidence - eviBox.src = safeTags(client.evidences[this.chatmsg.evidence - 1].icon); - - eviBox.style.width = 'auto'; - eviBox.style.height = '36.5%'; - eviBox.style.opacity = 1; - - this.testimonyAudio.src = `${AO_HOST}sounds/general/sfx-evidenceshoop.opus`; - this.testimonyAudio.play(); - - if (this.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'; - } - } - - resizeChatbox(); - - const chatContainerBox = document.getElementById('client_chatcontainer'); - chatContainerBox.style.opacity = 1; - - chatBoxInner.className = `text_${this.colors[this.chatmsg.color]}`; - - - if (this.chatmsg.preanimdelay === 0) { - shoutSprite.style.opacity = 0; - shoutSprite.style.animation = ''; - } - - if (this.chatmsg.other_name) { - setEmote(AO_HOST, this, pairName, pairEmote, '(a)', true, this.chatmsg.side); - pairLayers.style.opacity = 1; - } else { - pairLayers.style.opacity = 0; - } - - setEmote(AO_HOST, this, charName, charEmote, '(b)', false, this.chatmsg.side); - charLayers.style.opacity = 1; - - if (this.textnow === this.chatmsg.content) { - setEmote(AO_HOST, this, charName, charEmote, '(a)', false, this.chatmsg.side); - charLayers.style.opacity = 1; - waitingBox.style.opacity = 1; - this._animating = false; - clearTimeout(this.updater); - } - } else if (this.textnow !== this.chatmsg.content) { - this.handleTextTick(charLayers) - } - } - - if (!this.sfxplayed && this.chatmsg.snddelay + this.shoutTimer >= this.tickTimer) { - this.sfxplayed = 1; - if (this.chatmsg.sound !== '0' && this.chatmsg.sound !== '1' && this.chatmsg.sound !== '' && this.chatmsg.sound !== undefined && (this.chatmsg.type == 1 || this.chatmsg.type == 2 || this.chatmsg.type == 6)) { - this.playSFX(`${AO_HOST}sounds/general/${encodeURI(this.chatmsg.sound.toLowerCase())}.opus`, this.chatmsg.looping_sfx); - } - } - this.tickTimer += UPDATE_INTERVAL; - } -} - -/** - * Triggered when the Return key is pressed on the out-of-character chat input box. - * @param {KeyboardEvent} event - */ -export function onOOCEnter(event) { - if (event.keyCode === 13) { - client.sendOOC(document.getElementById('client_oocinputbox').value); - document.getElementById('client_oocinputbox').value = ''; - } -} -window.onOOCEnter = onOOCEnter; - -/** - * Triggered when the user click replay GOOOOO - * @param {KeyboardEvent} event - */ -export function onReplayGo(_event) { - client.handleReplay(); -} -window.onReplayGo = onReplayGo; - -/** - * Triggered when the Return key is pressed on the in-character chat input box. - * @param {KeyboardEvent} event - */ -export function onEnter(event) { - if (event.keyCode === 13) { - const mychar = client.character; - const myemo = client.emote; - const evi = client.evidence; - const flip = ((document.getElementById('button_flip').classList.contains('dark')) ? 1 : 0); - const flash = ((document.getElementById('button_flash').classList.contains('dark')) ? 1 : 0); - const screenshake = ((document.getElementById('button_shake').classList.contains('dark')) ? 1 : 0); - const noninterrupting_preanim = ((document.getElementById('check_nonint').checked) ? 1 : 0); - const looping_sfx = ((document.getElementById('check_loopsfx').checked) ? 1 : 0); - const color = document.getElementById('textcolor').value; - const showname = document.getElementById('ic_chat_name').value; - const text = document.getElementById('client_inputbox').value; - const pairchar = document.getElementById('pair_select').value; - const pairoffset = document.getElementById('pair_offset').value; - const pairyoffset = document.getElementById('pair_y_offset').value; - const myrole = document.getElementById('role_select').value ? document.getElementById('role_select').value : mychar.side; - const additive = ((document.getElementById('check_additive').checked) ? 1 : 0); - const effect = document.getElementById('effect_select').value; - - let sfxname = '0'; - let sfxdelay = 0; - let emote_mod = myemo.zoom; - if (document.getElementById('sendsfx').checked) { - sfxname = myemo.sfx; - sfxdelay = myemo.sfxdelay; - } - - // not to overwrite a 5 from the ini or anything else - if (document.getElementById('sendpreanim').checked) { - if (emote_mod === 0) { emote_mod = 1; } - } else if (emote_mod === 1) { emote_mod = 0; } - - - client.sendIC( - 'chat', - myemo.preanim, - mychar.name, - myemo.emote, - text, - myrole, - sfxname, - emote_mod, - sfxdelay, - selectedShout, - evi, - flip, - flash, - color, - showname, - pairchar, - pairoffset, - pairyoffset, - noninterrupting_preanim, - looping_sfx, - screenshake, - '-', - '-', - '-', - additive, - effect, - ); - } -} -window.onEnter = onEnter; - -/** - * 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. - */ -function resetICParams() { - document.getElementById('client_inputbox').value = ''; - document.getElementById('button_flash').className = 'client_button'; - document.getElementById('button_shake').className = 'client_button'; - - document.getElementById('sendpreanim').checked = false; - - if (selectedShout) { - document.getElementById(`button_${selectedShout}`).className = 'client_button'; - selectedShout = 0; - } -} - -export function resetOffset(_event) { - document.getElementById('pair_offset').value = 0; - document.getElementById('pair_y_offset').value = 0; -} -window.resetOffset = resetOffset; - -/** - * Triggered when the music search bar is changed - * @param {MouseEvent} event - */ -export function musiclist_filter(_event) { - const musiclist_element = document.getElementById('client_musiclist'); - const searchname = document.getElementById('client_musicsearch').value; - - musiclist_element.innerHTML = ''; - - for (const trackname of client.musics) { - if (trackname.toLowerCase().indexOf(searchname.toLowerCase()) !== -1) { - const newentry = document.createElement('OPTION'); - newentry.text = trackname; - musiclist_element.options.add(newentry); - } - } -} -window.musiclist_filter = musiclist_filter; - -/** - * Triggered when an item on the music list is clicked. - * @param {MouseEvent} event - */ -export function musiclist_click(_event) { - const playtrack = document.getElementById('client_musiclist').value; - client.sendMusicChange(playtrack); - - // This is here so you can't actually select multiple tracks, - // even though the select tag has the multiple option to render differently - const musiclist_elements = document.getElementById('client_musiclist').selectedOptions; - for (let i = 0; i < musiclist_elements.length; i++) { - musiclist_elements[i].selected = false; - } -} -window.musiclist_click = musiclist_click; - -/** - * Triggered when a character in the mute list is clicked - * @param {MouseEvent} event - */ -export function mutelist_click(_event) { - const mutelist = 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; - -/** - * Triggered when the showname checkboc is clicked - * @param {MouseEvent} event - */ -export function showname_click(_event) { - setCookie('showname', document.getElementById('showname').checked); - setCookie('ic_chat_name', document.getElementById('ic_chat_name').value); - - const css_s = document.getElementById('nameplate_setting'); - - if (document.getElementById('showname').checked) { css_s.href = 'styles/shownames.css'; } else { css_s.href = 'styles/nameplates.css'; } -} -window.showname_click = showname_click; - -/** - * Triggered when an item on the area list is clicked. - * @param {MouseEvent} event - */ -export function area_click(el) { - const area = client.areas[el.id.substr(4)].name; - client.sendMusicChange(area); - - const areaHr = document.createElement('div'); - areaHr.className = 'hrtext'; - areaHr.textContent = `switched to ${el.textContent}`; - document.getElementById('client_log').appendChild(areaHr); -} -window.area_click = area_click; - -/** - * Triggered by the music volume slider. - */ -export function changeMusicVolume() { - viewport.musicVolume = document.getElementById('client_mvolume').value; - setCookie('musicVolume', document.getElementById('client_mvolume').value); -} -window.changeMusicVolume = changeMusicVolume; - -/** - * Triggered by the testimony volume slider. - */ -export function changeTestimonyVolume() { - setCookie('testimonyVolume', document.getElementById('client_testimonyaudio').volume); -} -window.changeTestimonyVolume = changeTestimonyVolume; - -/** - * Triggered by the blip volume slider. - */ -export function changeBlipVolume() { - const blipVolume = document.getElementById('client_bvolume').value; - viewport.blipChannels.forEach((channel) => channel.volume = blipVolume); - setCookie('blipVolume', document.getElementById('client_bvolume').value); -} -window.changeBlipVolume = changeBlipVolume; - -/** - * Triggered by the theme selector. - */ -export function reloadTheme() { - viewport.theme = document.getElementById('client_themeselect').value; - setCookie('theme', viewport.theme); - document.getElementById('client_theme').href = `styles/${viewport.theme}.css`; -} -window.reloadTheme = reloadTheme; - -/** - * Triggered by a changed callword list - */ -export function changeCallwords() { - client.callwords = document.getElementById('client_callwords').value.split('\n'); - setCookie('callwords', client.callwords); -} -window.changeCallwords = changeCallwords; - -/** - * Triggered by the modcall sfx dropdown - */ -export function modcall_test() { - client.handleZZ('test#test'.split('#')); -} -window.modcall_test = modcall_test; - -/** - * Triggered by the ini button. - */ -export async function iniedit() { - const ininame = document.getElementById('client_ininame').value; - const inicharID = client.charID; - await client.handleCharacterInfo(ininame.split('&'), inicharID); - client.handlePV((`PV#0#CID#${inicharID}`).split('#')); -} -window.iniedit = iniedit; - -/** - * Triggered by the pantilt checkbox - */ -export async function switchPanTilt(addcheck) { - const background = document.getElementById('client_fullview'); - if (addcheck === 1) { - document.getElementById('client_pantilt').checked = true; - document.getElementById('client_court').style.display = ''; - } else if (addcheck === 2) { - document.getElementById('client_pantilt').checked = false; - document.getElementById('client_court').style.display = 'none'; - } - if (document.getElementById('client_pantilt').checked) { - background.style.transition = '0.5s ease-in-out'; - } else { - background.style.transition = 'none'; - } -} -window.switchPanTilt = switchPanTilt; - -/** - * Triggered by the change aspect ratio checkbox - */ -export async function switchAspectRatio() { - const background = document.getElementById('client_background'); - const offsetCheck = document.getElementById('client_hdviewport_offset'); - if (document.getElementById('client_hdviewport').checked) { - background.style.paddingBottom = '56.25%'; - offsetCheck.disabled = false; - } else { - background.style.paddingBottom = '75%'; - offsetCheck.disabled = true; - } -} -window.switchAspectRatio = switchAspectRatio; - -/** - * Triggered by the change aspect ratio checkbox - */ -export async function switchChatOffset() { - const container = document.getElementById('client_chatcontainer'); - if (document.getElementById('client_hdviewport_offset').checked) { - container.style.width = '80%'; - container.style.left = '10%'; - } else { - container.style.width = '100%'; - container.style.left = 0; - } -} -window.switchChatOffset = switchChatOffset; - -/** - * Triggered when a character icon is clicked in the character selection menu. - * @param {MouseEvent} event - */ -export function changeCharacter(_event) { - document.getElementById('client_charselect').style.display = 'block'; - document.getElementById('client_emo').innerHTML = ''; -} -window.changeCharacter = changeCharacter; - -/** - * Triggered when there was an error loading a character sprite. - * @param {HTMLImageElement} image the element containing the missing image - */ -export function charError(image) { - console.warn(`${image.src} is missing from webAO`); - image.src = transparentPng; - return true; -} -window.charError = charError; - -/** - * Triggered when there was an error loading a generic sprite. - * @param {HTMLImageElement} image the element containing the missing image - */ -export function imgError(image) { - image.onerror = ''; - image.src = ''; // unload so the old sprite doesn't persist - return true; -} -window.imgError = imgError; - -/** - * Triggered when there was an error loading a sound - * @param {HTMLImageElement} image the element containing the missing sound - */ -export function opusCheck(channel) { - const audio = channel.src - if (audio === '') { - return - } - console.info(`failed to load sound ${channel.src}`); - let oldsrc = ''; - oldsrc = channel.src; - if (!oldsrc.endsWith('.opus')) { - newsrc = oldsrc.replace('.mp3', '.opus'); - newsrc = newsrc.replace('.wav', '.opus'); - channel.src = newsrc; // unload so the old sprite doesn't persist - } -} -window.opusCheck = opusCheck; - -/** - * Triggered when the reconnect button is pushed. - */ -export function ReconnectButton() { - client.cleanup(); - client = new Client(serverIP); - if (client) { - mode = 'join'; // HACK: see client.onOpen - - document.getElementById('client_error').style.display = 'none'; - } -} -window.ReconnectButton = ReconnectButton; - -/** - * 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 - */ -function appendICLog(msg, 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.classList = 'iclog_name iclog_nameplate'; - nameplateField.appendChild(document.createTextNode(nameplate)); - - shownameField.classList = '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) { - client.callwords.forEach(testCallword); - - function testCallword(item) { - if (item !== '' && message.toLowerCase().includes(item.toLowerCase())) { - viewport.sfxaudio.pause(); - viewport.sfxaudio.src = `${AO_HOST}sounds/general/sfx-gallery.opus`; - viewport.sfxaudio.play(); - } - } -} - -/** - * Triggered when the music search bar is changed - * @param {MouseEvent} event - */ -export function chartable_filter(_event) { - const searchname = document.getElementById('client_charactersearch').value; - - client.chars.forEach((character, charid) => { - const demothing = document.getElementById(`demo_${charid}`); - if (character.name.toLowerCase().indexOf(searchname.toLowerCase()) === -1) { - demothing.style.display = 'none'; - } else { - demothing.style.display = 'inline-block'; - } - }); -} -window.chartable_filter = chartable_filter; - -/** - * Requests to play as a character. - * @param {number} ccharacter the character ID; if this is a large number, - * then spectator is chosen instead. - */ -export function pickChar(ccharacter) { - if (ccharacter === -1) { - // Spectator - document.getElementById('client_charselect').style.display = 'none'; - } else { - client.sendCharacter(ccharacter); - } -} -window.pickChar = pickChar; - -/** - * Highlights and selects an emotion for in-character chat. - * @param {string} emo the new emotion to be selected - */ -export function pickEmotion(emo) { - try { - if (client.selectedEmote !== -1) { - document.getElementById(`emo_${client.selectedEmote}`).classList = 'emote_button'; - } - } catch (err) { - // do nothing - } - client.selectedEmote = emo; - document.getElementById(`emo_${emo}`).classList = 'emote_button dark'; - - document.getElementById('sendsfx').checked = (client.emote.sfx.length > 1); - - document.getElementById('sendpreanim').checked = (client.emote.zoom == 1); -} -window.pickEmotion = pickEmotion; - -/** - * Highlights and selects an evidence for in-character chat. - * @param {string} evidence the evidence to be presented - */ -export function pickEvidence(evidenceID) { - const evidence = Number(evidenceID); - if (client.selectedEvidence !== evidence) { - // Update selected evidence - if (client.selectedEvidence > 0) { - document.getElementById(`evi_${client.selectedEvidence}`).className = 'evi_icon'; - } - document.getElementById(`evi_${evidence}`).className = 'evi_icon dark'; - client.selectedEvidence = evidence; - - // Show evidence on information window - document.getElementById('evi_name').value = client.evidences[evidence - 1].name; - document.getElementById('evi_desc').value = client.evidences[evidence - 1].desc; - - // Update icon - const icon_id = getIndexFromSelect('evi_select', client.evidences[evidence - 1].filename); - document.getElementById('evi_select').selectedIndex = icon_id; - if (icon_id === 0) { - document.getElementById('evi_filename').value = client.evidences[evidence - 1].filename; - } - updateEvidenceIcon(); - - // Update button - document.getElementById('evi_add').className = 'client_button hover_button inactive'; - document.getElementById('evi_edit').className = 'client_button hover_button'; - document.getElementById('evi_cancel').className = 'client_button hover_button'; - document.getElementById('evi_del').className = 'client_button hover_button'; - } else { - cancelEvidence(); - } -} -window.pickEvidence = pickEvidence; - -/** - * Add evidence. - */ -export function addEvidence() { - const evidence_select = document.getElementById('evi_select'); - client.sendPE( - document.getElementById('evi_name').value, - document.getElementById('evi_desc').value, - evidence_select.selectedIndex === 0 - ? document.getElementById('evi_filename').value - : evidence_select.options[evidence_select.selectedIndex].text, - ); - cancelEvidence(); -} -window.addEvidence = addEvidence; - -/** - * Edit selected evidence. - */ -export function editEvidence() { - const evidence_select = document.getElementById('evi_select'); - const id = parseInt(client.selectedEvidence) - 1; - client.sendEE( - id, - document.getElementById('evi_name').value, - document.getElementById('evi_desc').value, - evidence_select.selectedIndex === 0 - ? document.getElementById('evi_filename').value - : evidence_select.options[evidence_select.selectedIndex].text, - ); - cancelEvidence(); -} -window.editEvidence = editEvidence; - -/** - * Delete selected evidence. - */ -export function deleteEvidence() { - const id = parseInt(client.selectedEvidence) - 1; - client.sendDE(id); - cancelEvidence(); -} -window.deleteEvidence = deleteEvidence; - -/** - * Cancel evidence selection. - */ -export function cancelEvidence() { - // Clear evidence data - if (client.selectedEvidence > 0) { - document.getElementById(`evi_${client.selectedEvidence}`).className = 'evi_icon'; - } - client.selectedEvidence = 0; - - // Clear evidence on information window - document.getElementById('evi_select').selectedIndex = 0; - updateEvidenceIcon(); // Update icon widget - document.getElementById('evi_filename').value = ''; - document.getElementById('evi_name').value = ''; - document.getElementById('evi_desc').value = ''; - document.getElementById('evi_preview').src = `${AO_HOST}misc/empty.png`; // Clear icon - - // Update button - document.getElementById('evi_add').className = 'client_button hover_button'; - document.getElementById('evi_edit').className = 'client_button hover_button inactive'; - document.getElementById('evi_cancel').className = 'client_button hover_button inactive'; - document.getElementById('evi_del').className = 'client_button hover_button inactive'; -} -window.cancelEvidence = cancelEvidence; - -/** - * Find index of anything in select box. - * @param {string} select_box the select element name - * @param {string} value the value that need to be compared - */ -export function getIndexFromSelect(select_box, value) { - // Find if icon alraedy existed in select box - const select_element = document.getElementById(select_box); - for (let i = 1; i < select_element.length; ++i) { - if (select_element.options[i].value === value) { - return i; - } - } - return 0; -} -window.getIndexFromSelect = getIndexFromSelect; - -/** - * Set the style of the chatbox - */ -export function setChatbox(style) { - const chatbox_theme = document.getElementById('chatbox_theme'); - const selected_theme = document.getElementById('client_chatboxselect').value; - setCookie('chatbox', selected_theme); - if (selected_theme === 'dynamic') { - if (chatbox_arr.includes(style)) { - chatbox_theme.href = `styles/chatbox/${style}.css`; - } else { - chatbox_theme.href = 'styles/chatbox/aa.css'; - } - } else { - chatbox_theme.href = `styles/chatbox/${selected_theme}.css`; - } -} -window.setChatbox = setChatbox; - -/** - * Set the font size for the chatbox - */ -export function resizeChatbox() { - const chatContainerBox = document.getElementById('client_chatcontainer'); - const gameHeight = document.getElementById('client_background').offsetHeight; - - chatContainerBox.style.fontSize = `${(gameHeight * 0.0521).toFixed(1)}px`; -} -window.resizeChatbox = resizeChatbox; - -/** - * Update evidence icon. - */ -export function updateEvidenceIcon() { - const evidence_select = document.getElementById('evi_select'); - const evidence_filename = document.getElementById('evi_filename'); - const evidence_iconbox = document.getElementById('evi_preview'); - - if (evidence_select.selectedIndex === 0) { - evidence_filename.style.display = 'initial'; - evidence_iconbox.src = `${AO_HOST}evidence/${encodeURI(evidence_filename.value.toLowerCase())}`; - } else { - evidence_filename.style.display = 'none'; - evidence_iconbox.src = `${AO_HOST}evidence/${encodeURI(evidence_select.value.toLowerCase())}`; - } -} -window.updateEvidenceIcon = updateEvidenceIcon; - -/** - * Update evidence icon. - */ -export function updateActionCommands(side) { - if (side === 'jud') { - document.getElementById('judge_action').style.display = 'inline-table'; - document.getElementById('no_action').style.display = 'none'; - } else { - document.getElementById('judge_action').style.display = 'none'; - document.getElementById('no_action').style.display = 'inline-table'; - } - - // Update role selector - for (let i = 0, role_select = document.getElementById('role_select').options; i < role_select.length; i++) { - if (side === role_select[i].value) { - role_select.selectedIndex = i; - return; - } - } -} -window.updateActionCommands = updateActionCommands; - -/** - * Change background via OOC. - */ -export function changeBackgroundOOC() { - const selectedBG = document.getElementById('bg_select'); - const changeBGCommand = "bg $1"; - const bgFilename = document.getElementById('bg_filename'); - - let filename = ''; - if (selectedBG.selectedIndex === 0) { - filename = bgFilename.value; - } else { - filename = selectedBG.value; - } - - if (mode === 'join') { client.sendOOC(`/${changeBGCommand.replace('$1', filename)}`); } else if (mode === 'replay') { client.sendSelf(`BN#${filename}#%`); } -} -window.changeBackgroundOOC = changeBackgroundOOC; - -/** - * Change role via OOC. - */ -export function changeRoleOOC() { - const new_role = document.getElementById('role_select').value; - - client.sendOOC(`/pos ${new_role}`); - client.sendServer(`SP#${new_role}#%`); - updateActionCommands(new_role); -} -window.changeRoleOOC = changeRoleOOC; - -/** - * Random character via OOC. - */ -export function randomCharacterOOC() { - client.sendOOC(`/randomchar`); -} -window.randomCharacterOOC = randomCharacterOOC; - -/** - * Call mod. - */ -export function callMod() { - let modcall; - if (extrafeatures.includes('modcall_reason')) { - modcall = prompt('Please enter the reason for the modcall', ''); - } - if (modcall == null || modcall === '') { - // cancel - } else { - client.sendZZ(modcall); - } -} -window.callMod = callMod; - -/** - * Declare witness testimony. - */ -export function initWT() { - client.sendRT('testimony1'); -} -window.initWT = initWT; - -/** - * Declare cross examination. - */ -export function initCE() { - client.sendRT('testimony2'); -} -window.initCE = initCE; - -/** - * Declare the defendant not guilty - */ -export function notguilty() { - client.sendRT('judgeruling#0'); -} -window.notguilty = notguilty; - -/** - * Declare the defendant not guilty - */ -export function guilty() { - client.sendRT('judgeruling#1'); -} -window.guilty = guilty; - -/** - * Increment defense health point. - */ -export function addHPD() { - client.sendHP(1, String(parseInt(client.hp[0]) + 1)); -} -window.addHPD = addHPD; - -/** - * Decrement defense health point. - */ -export function redHPD() { - client.sendHP(1, String(parseInt(client.hp[0]) - 1)); -} -window.redHPD = redHPD; - -/** - * Increment prosecution health point. - */ -export function addHPP() { - client.sendHP(2, String(parseInt(client.hp[1]) + 1)); -} -window.addHPP = addHPP; - -/** - * Decrement prosecution health point. - */ -export function redHPP() { - client.sendHP(2, String(parseInt(client.hp[1]) - 1)); -} -window.redHPP = redHPP; - -/** - * Update background preview. - */ -export function updateBackgroundPreview() { - const background_select = document.getElementById('bg_select'); - const background_filename = document.getElementById('bg_filename'); - const background_preview = document.getElementById('bg_preview'); - - if (background_select.selectedIndex === 0) { - background_filename.style.display = 'initial'; - background_preview.src = `${AO_HOST}background/${encodeURI(background_filename.value.toLowerCase())}/defenseempty.png`; - } else { - background_filename.style.display = 'none'; - background_preview.src = `${AO_HOST}background/${encodeURI(background_select.value.toLowerCase())}/defenseempty.png`; - } -} -window.updateBackgroundPreview = updateBackgroundPreview; - -/** - * Highlights and selects a menu. - * @param {string} menu the menu to be selected - */ -export function toggleMenu(menu) { - if (menu !== selectedMenu) { - document.getElementById(`menu_${menu}`).className = 'menu_button active'; - document.getElementById(`content_${menu}`).className = 'menu_content active'; - document.getElementById(`menu_${selectedMenu}`).className = 'menu_button'; - document.getElementById(`content_${selectedMenu}`).className = 'menu_content'; - selectedMenu = menu; - } -} -window.toggleMenu = toggleMenu; - -/** - * Highlights and selects a shout for in-character chat. - * If the same shout button is selected, then the shout is canceled. - * @param {string} shout the new shout to be selected - */ -export function toggleShout(shout) { - if (shout === selectedShout) { - document.getElementById(`button_${shout}`).className = 'client_button'; - selectedShout = 0; - } else { - document.getElementById(`button_${shout}`).className = 'client_button dark'; - if (selectedShout) { - document.getElementById(`button_${selectedShout}`).className = 'client_button'; - } - selectedShout = shout; - } -} -window.toggleShout = toggleShout; -export default Client diff --git a/webAO/client.ts b/webAO/client.ts new file mode 100644 index 0000000..04a1814 --- /dev/null +++ b/webAO/client.ts @@ -0,0 +1,3283 @@ +/* + * Glorious webAO + * made by sD, refactored by oldmud0 and Qubrick + * credits to aleks for original idea and source +*/ + +import FingerprintJS from '@fingerprintjs/fingerprintjs'; +import { EventEmitter } from 'events'; +import tryUrls from './utils/tryUrls' +import { + escapeChat, encodeChat, prepChat, safeTags, +} from './encoding'; +import mlConfig from './utils/aoml'; +// Load some defaults for the background and evidence dropdowns +import vanilla_character_arr from './constants/characters.js'; +import vanilla_music_arr from './constants/music.js'; +import vanilla_background_arr from './constants/backgrounds.js'; +import vanilla_evidence_arr from './constants/evidence.js'; + +import chatbox_arr from './styles/chatbox/chatboxes.js'; +import iniParse from './iniParse'; +import getCookie from './utils/getCookie.js'; +import setCookie from './utils/setCookie.js'; +import { request } from './services/request.js'; +import { changeShoutVolume, changeSFXVolume, changeTestimonyVolume } from './dom/changeVolume.js'; +import setEmote from './client/setEmote.js'; +import fileExists from './utils/fileExists.js'; +import queryParser from './utils/queryParser.js'; +import getAnimLength from './utils/getAnimLength.js'; +import getResources from './utils/getResources.js'; +import transparentPng from './constants/transparentPng'; +import downloadFile from './services/downloadFile' +const version = process.env.npm_package_version; + +let client; +let viewport; +// Get the arguments from the URL bar +let { + ip: serverIP, mode, asset, theme, +} = queryParser(); +// Unless there is an asset URL specified, use the wasabi one +const DEFAULT_HOST = 'http://attorneyoffline.de/base/'; +let AO_HOST = asset || DEFAULT_HOST; +const THEME = theme || 'default'; + +const attorneyMarkdown = mlConfig(AO_HOST) + +const UPDATE_INTERVAL = 60; + +/** + * Toggles AO1-style loading using paginated music packets for mobile platforms. + * The old loading uses more smaller packets instead of a single big one, + * which caused problems on low-memory devices in the past. + */ +let oldLoading = false; + +// presettings +let selectedMenu = 1; +let selectedShout = 0; + +let extrafeatures = []; + +let hdid; + +declare global { + interface Window { + toggleShout: (shout: number) => void; + toggleMenu: (menu: number) => void; + updateBackgroundPreview: () => void; + redHPP: () => void; + addHPP: () => void; + redHPD: () => void; + addHPD: () => void; + guilty: () => void; + notguilty: () => void; + initCE: () => void; + initWT: () => void; + callMod: () => void; + randomCharacterOOC: () => void; + changeRoleOOC: () => void; + changeBackgroundOOC: () => void; + updateActionCommands: (side: string) => void; + updateEvidenceIcon: () => void; + resizeChatbox: () => void; + setChatbox: (style: string) => void; + getIndexFromSelect: (select_box: string, value: string) => Number; + cancelEvidence: () => void; + deleteEvidence: () => void; + editEvidence: () => void; + addEvidence: () => void; + pickEvidence: (evidence: any) => void; + pickEmotion: (emo: any) => void; + pickChar: (ccharacter: any) => void; + chartable_filter: (_event: any) => void; + ReconnectButton: (_event: any) => void; + opusCheck: (channel: any) => void; + imgError: (image: any) => void; + charError: (image: any) => void; + changeCharacter: (_event: any) => void; + switchChatOffset: () => void; + switchAspectRatio: () => void; + switchPanTilt: (addcheck: number) => void; + iniedit: () => void; + modcall_test: () => void; + reloadTheme: () => void; + changeCallwords: () => void; + changeBlipVolume: () => void; + changeMusicVolume: () => void; + area_click: (el: any) => void; + showname_click: (_event: any) => void; + mutelist_click: (_event: any) => void; + musiclist_click: (_event: any) => void; + musiclist_filter: (_event: any) => void; + resetOffset: (_event: any) => void; + onEnter: (event: any) => void; + onReplayGo: (_event: any) => void; + onOOCEnter: (_event: any) => void; + } +} + +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(); +fpPromise + .then((fp) => fp.get()) + .then((result) => { + hdid = result.visitorId; + client = new Client(serverIP); + viewport = new Viewport(); + + isLowMemory(); + client.loadResources(); + }); + +let lastICMessageTime = new Date(0); + +class Client extends EventEmitter { + serv: any; + hp: number[]; + playerID: number; + charID: number; + char_list_length: number; + evidence_list_length: number; + music_list_length: number; + testimonyID: number; + chars: any; + emotes: any; + evidences: any; + areas: any; + musics: any; + musics_time: boolean; + callwords: string[]; + banned: boolean; + resources: any; + selectedEmote: number; + selectedEvidence: number; + checkUpdater: any; + _lastTimeICReceived: any; + + constructor(address) { + super(); + if (mode !== 'replay') { + this.serv = new WebSocket(`ws://${address}`); + // Assign the websocket events + this.serv.addEventListener('open', this.emit.bind(this, 'open')); + this.serv.addEventListener('close', this.emit.bind(this, 'close')); + this.serv.addEventListener('message', this.emit.bind(this, 'message')); + this.serv.addEventListener('error', this.emit.bind(this, 'error')); + } else { + this.joinServer(); + } + + this.on('open', this.onOpen.bind(this)); + this.on('close', this.onClose.bind(this)); + this.on('message', this.onMessage.bind(this)); + this.on('error', this.onError.bind(this)); + + // Preset some of the variables + + this.hp = [0, 0]; + + this.playerID = 1; + this.charID = -1; + this.char_list_length = 0; + this.evidence_list_length = 0; + this.music_list_length = 0; + this.testimonyID = 0; + + this.chars = []; + this.emotes = []; + this.evidences = []; + this.areas = []; + this.musics = []; + + this.musics_time = false; + + this.callwords = []; + + this.banned = false; + + this.resources = getResources(AO_HOST, THEME); + + this.selectedEmote = -1; + this.selectedEvidence = 0; + + this.checkUpdater = null; + + /** + * Assign handlers for all commands + * If you implement a new command, you need to add it here + */ + this.on('MS', this.handleMS.bind(this)); + this.on('CT', this.handleCT.bind(this)); + this.on('MC', this.handleMC.bind(this)); + this.on('RMC', this.handleRMC.bind(this)); + this.on('CI', this.handleCI.bind(this)); + this.on('SC', this.handleSC.bind(this)); + this.on('EI', this.handleEI.bind(this)); + this.on('FL', this.handleFL.bind(this)); + this.on('LE', this.handleLE.bind(this)); + this.on('EM', this.handleEM.bind(this)); + this.on('FM', this.handleFM.bind(this)); + this.on('FA', this.handleFA.bind(this)); + this.on('SM', this.handleSM.bind(this)); + this.on('MM', this.handleMM.bind(this)); + this.on('BD', this.handleBD.bind(this)); + this.on('BB', this.handleBB.bind(this)); + this.on('KB', this.handleKB.bind(this)); + this.on('KK', this.handleKK.bind(this)); + this.on('DONE', this.handleDONE.bind(this)); + this.on('BN', this.handleBN.bind(this)); + this.on('HP', this.handleHP.bind(this)); + this.on('RT', this.handleRT.bind(this)); + this.on('TI', this.handleTI.bind(this)); + this.on('ZZ', this.handleZZ.bind(this)); + this.on('HI', this.handleHI.bind(this)); + this.on('ID', this.handleID.bind(this)); + this.on('PN', this.handlePN.bind(this)); + this.on('SI', this.handleSI.bind(this)); + this.on('ARUP', this.handleARUP.bind(this)); + this.on('askchaa', this.handleaskchaa.bind(this)); + this.on('CC', this.handleCC.bind(this)); + this.on('RC', this.handleRC.bind(this)); + this.on('RM', this.handleRM.bind(this)); + this.on('RD', this.handleRD.bind(this)); + this.on('CharsCheck', this.handleCharsCheck.bind(this)); + this.on('PV', this.handlePV.bind(this)); + this.on('ASS', this.handleASS.bind(this)); + this.on('CHECK', () => { }); + this.on('CH', () => { }); + + this._lastTimeICReceived = new Date(0); + } + + /** + * Gets the current player's character. + */ + get character() { + return this.chars[this.charID]; + } + + /** + * Gets the player's currently selected emote. + */ + get emote() { + return this.emotes[this.selectedEmote]; + } + + /** + * Gets the current evidence ID unless the player doesn't want to present any evidence + */ + get evidence() { + return (document.getElementById('button_present').classList.contains('dark')) ? this.selectedEvidence : 0; + } + + /** + * Hook for sending messages to the server + * @param {string} message the message to send + */ + sendServer(message) { + mode === 'replay' ? this.sendSelf(message) : this.serv.send(message); + } + + /** + * Hook for sending messages to the client + * @param {string} message the message to send + */ + handleSelf(message) { + const message_event = new MessageEvent('websocket', { data: message }); + setTimeout(() => this.onMessage(message_event), 1); + } + + /** + * Hook for sending messages to the client + * @param {string} message the message to send + */ + sendSelf(message) { + (document.getElementById('client_ooclog')).value += `${message}\r\n`; + this.handleSelf(message); + } + + /** + * Sends an out-of-character chat message. + * @param {string} message the message to send + */ + sendOOC(message) { + setCookie('OOC_name', (document.getElementById('OOC_name')).value); + const oocName = `${escapeChat(encodeChat((document.getElementById('OOC_name')).value))}`; + const oocMessage = `${escapeChat(encodeChat(message))}`; + + const commands = { + '/save_chatlog': this.saveChatlogHandle + } + const commandsMap = new Map(Object.entries(commands)) + + if (oocMessage && commandsMap.has(oocMessage.toLowerCase())) { + try { + commandsMap.get(oocMessage.toLowerCase())() + } catch (e) { + // Command Not Recognized + } + } else { + this.sendServer(`CT#${oocName}#${oocMessage}#%`); + } + } + + /** + * Sends an in-character chat message. + * @param {string} deskmod currently unused + * @param {string} speaking who is speaking + * @param {string} name the name of the current character + * @param {string} silent whether or not it's silent + * @param {string} message the message to be sent + * @param {string} side the name of the side in the background + * @param {string} sfx_name the name of the sound effect + * @param {string} emote_modifier whether or not to zoom + * @param {number} sfx_delay the delay (in milliseconds) to play the sound effect + * @param {string} objection_modifier the number of the shout to play + * @param {string} evidence the filename of evidence to show + * @param {number} flip change to 1 to reverse sprite for position changes + * @param {number} realization screen flash effect + * @param {number} text_color text color + * @param {string} showname custom name to be displayed (optional) + * @param {number} other_charid paired character (optional) + * @param {number} self_offset offset to paired character (optional) + * @param {number} noninterrupting_preanim play the full preanim (optional) + */ + sendIC( + deskmod, + preanim, + name, + emote, + message, + side, + sfx_name, + emote_modifier, + sfx_delay, + objection_modifier, + evidence, + flip, + realization, + text_color, + showname, + other_charid, + self_hoffset, + self_yoffset, + noninterrupting_preanim, + looping_sfx, + screenshake, + frame_screenshake, + frame_realization, + frame_sfx, + additive, + effect, + ) { + let extra_cccc = ''; + let other_emote = ''; + let other_offset = ''; + let extra_27 = ''; + let extra_28 = ''; + + if (extrafeatures.includes('cccc_ic_support')) { + const self_offset = extrafeatures.includes('y_offset') ? `${self_hoffset}${self_yoffset}` : self_hoffset; // HACK: this should be an & but client fucked it up and all the servers adopted it + if (mode === 'replay') { + other_emote = '##'; + other_offset = '#0#0'; + } + extra_cccc = `${showname}#${other_charid}${other_emote}#${self_offset}${other_offset}#${noninterrupting_preanim}#`; + + if (extrafeatures.includes('looping_sfx')) { + extra_27 = `${looping_sfx}#${screenshake}#${frame_screenshake}#${frame_realization}#${frame_sfx}#`; + if (extrafeatures.includes('effects')) { + extra_28 = `${additive}#${effect}#`; + } + } + } + + const serverMessage = `MS#${deskmod}#${preanim}#${name}#${emote}` + + `#${escapeChat(encodeChat(message))}#${side}#${sfx_name}#${emote_modifier}` + + `#${this.charID}#${sfx_delay}#${objection_modifier}#${evidence}#${flip}#${realization}#${text_color}#${extra_cccc}${extra_27}${extra_28}%`; + + this.sendServer(serverMessage); + if (mode === 'replay') { + (document.getElementById('client_ooclog')).value += `wait#${(document.getElementById('client_replaytimer')).value}#%\r\n`; + } + } + + /** + * Sends add evidence command. + * @param {string} evidence name + * @param {string} evidence description + * @param {string} evidence image filename + */ + sendPE(name, desc, img) { + this.sendServer(`PE#${escapeChat(encodeChat(name))}#${escapeChat(encodeChat(desc))}#${img}#%`); + } + + /** + * Sends edit evidence command. + * @param {number} evidence id + * @param {string} evidence name + * @param {string} evidence description + * @param {string} evidence image filename + */ + sendEE(id, name, desc, img) { + this.sendServer(`EE#${id}#${escapeChat(encodeChat(name))}#${escapeChat(encodeChat(desc))}#${img}#%`); + } + + /** + * Sends delete evidence command. + * @param {number} evidence id + */ + sendDE(id) { + this.sendServer(`DE#${id}#%`); + } + + /** + * Sends health point command. + * @param {number} side the position + * @param {number} hp the health point + */ + sendHP(side, hp) { + this.sendServer(`HP#${side}#${hp}#%`); + } + + /** + * Sends call mod command. + * @param {string} message to mod + */ + sendZZ(msg) { + if (extrafeatures.includes('modcall_reason')) { + this.sendServer(`ZZ#${msg}#%`); + } else { + this.sendServer('ZZ#%'); + } + } + + /** + * Sends testimony command. + * @param {string} testimony type + */ + sendRT(testimony) { + if (this.chars[this.charID].side === 'jud') { + this.sendServer(`RT#${testimony}#%`); + } + } + + /** + * Requests to change the music to the specified track. + * @param {string} track the track ID + */ + sendMusicChange(track) { + this.sendServer(`MC#${track}#${this.charID}#%`); + } + + /** + * Begins the handshake process by sending an identifier + * to the server. + */ + joinServer() { + this.sendServer(`HI#${hdid}#%`); + this.sendServer('ID#webAO#webAO#%'); + if (mode !== 'replay') { this.checkUpdater = setInterval(() => this.sendCheck(), 5000); } + } + + /** + * Load game resources and stored settings. + */ + loadResources() { + document.getElementById('client_version').innerText = `version ${version}`; + + // Load background array to select + const background_select = 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 = 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 + (document.getElementById('OOC_name')).value = getCookie('OOC_name') || `web${String(Math.random() * 100 + 10)}`; + + // Read cookies and set the UI to its values + const cookietheme = getCookie('theme') || 'default'; + + (document.querySelector(`#client_themeselect [value="${cookietheme}"]`)).selected = true; + reloadTheme(); + + const cookiechatbox = getCookie('chatbox') || 'dynamic'; + + (document.querySelector(`#client_chatboxselect [value="${cookiechatbox}"]`)).selected = true; + setChatbox(cookiechatbox); + + (document.getElementById('client_mvolume')).value = getCookie('musicVolume') || '1'; + changeMusicVolume(); + (document.getElementById('client_sfxaudio')).volume = Number(getCookie('sfxVolume')) || 1; + changeSFXVolume(); + (document.getElementById('client_shoutaudio')).volume = Number(getCookie('shoutVolume')) || 1; + changeShoutVolume(); + (document.getElementById('client_testimonyaudio')).volume = Number(getCookie('testimonyVolume')) || 1; + changeTestimonyVolume(); + (document.getElementById('client_bvolume')).value = getCookie('blipVolume') || '1'; + changeBlipVolume(); + + (document.getElementById('ic_chat_name')).value = getCookie('ic_chat_name'); + (document.getElementById('showname')).checked = Boolean(getCookie('showname')); + showname_click(0); + + (document.getElementById('client_callwords')).value = getCookie('callwords'); + } + + /** + * Requests to play as a specified character. + * @param {number} character the character ID + */ + sendCharacter(character) { + if (this.chars[character].name) { this.sendServer(`CC#${this.playerID}#${character}#web#%`); } + } + + /** + * Requests to select a music track. + * @param {number?} song the song to be played + */ + sendMusic(song) { + this.sendServer(`MC#${song}#${this.charID}#%`); + } + + /** + * Sends a keepalive packet. + */ + sendCheck() { + this.sendServer(`CH#${this.charID}#%`); + } + + /** + * Triggered when a connection is established to the server. + */ + onOpen(_e) { + client.joinServer(); + } + + /** + * Triggered when the connection to the server closes. + * @param {CloseEvent} e + */ + onClose(e) { + console.error(`The connection was closed: ${e.reason} (${e.code})`); + if (extrafeatures.length == 0 && this.banned === false) { + document.getElementById('client_errortext').textContent = 'Could not connect to the server'; + } + document.getElementById('client_error').style.display = 'flex'; + document.getElementById('client_loading').style.display = 'none'; + document.getElementById('error_id').textContent = e.code; + this.cleanup(); + } + + /** + * Triggered when a packet is received from the server. + * @param {MessageEvent} e + */ + onMessage(e) { + const msg = e.data; + console.debug(`S: ${msg}`); + + const lines = msg.split('%'); + + for (const msg of lines) { + if (msg === '') { break; } + + const args = msg.split('#'); + const header = args[0]; + + if (!this.emit(header, args)) { + console.warn(`Invalid packet header ${header}`); + } + } + } + + /** + * Triggered when an network error occurs. + * @param {ErrorEvent} e + */ + onError(e) { + console.error(`A network error occurred: ${e.reason} (${e.code})`); + document.getElementById('client_error').style.display = 'flex'; + document.getElementById('error_id').textContent = e.code; + this.cleanup(); + } + + /** + * Stop sending keepalives to the server. + */ + cleanup() { + clearInterval(this.checkUpdater); + + // the connection got rekt, get rid of the old musiclist + this.resetMusicList(); + document.getElementById('client_chartable').innerHTML = ''; + } + + /** + * Parse the lines in the OOC and play them + * @param {*} args packet arguments + */ + handleReplay() { + const ooclog = document.getElementById('client_ooclog'); + const rawLog = false; + let rtime: number = Number((document.getElementById('client_replaytimer')).value); + + const clines = ooclog.value.split(/\r?\n/); + if (clines[0]) { + const currentLine = String(clines[0]); + this.handleSelf(currentLine); + ooclog.value = clines.slice(1).join('\r\n'); + if (currentLine.substr(0, 4) === 'wait' && rawLog === false) { + rtime = Number(currentLine.split('#')[1]); + } else if (currentLine.substr(0, 2) !== 'MS') { + rtime = 0; + } + + setTimeout(() => onReplayGo(''), rtime); + } + } + + 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 + (document.getElementById('client_inputbox')).value = ''; + } + + /** + * Handles an in-character chat message. + * @param {*} args packet arguments + */ + handleMS(args) { + + // TODO: this if-statement might be a bug. + if (args[4] !== viewport.chatmsg.content) { + document.getElementById('client_inner_chat').innerHTML = ''; + + 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; + + try { + msg_nameplate = this.chars[char_id].showname; + msg_blips = this.chars[char_id].blips; + char_chatbox = this.chars[char_id].chat; + char_muted = this.chars[char_id].muted; + + if (this.chars[char_id].name !== char_name) { + console.info(`${this.chars[char_id].name} is iniediting to ${char_name}`); + const chargs = (`${char_name}&` + 'iniediter').split('&'); + this.handleCharacterInfo(chargs, char_id); + } + } catch (e) { + msg_nameplate = args[3]; + msg_blips = 'male'; + char_chatbox = 'default'; + char_muted = false; + console.error("we're still missing some character data"); + } + + 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]), + }; + + 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(''), // HACK: here as well, client is fucked and uses this instead of & + other_offset: args[21].split(''), + other_flip: Number(args[22]), + noninterrupting_preanim: Number(args[23]), + }; + chatmsg = Object.assign(extra_cccc, 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]), + }; + 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); + } + } 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: ['', '', ''], + }; + 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 === this.charID) { + resetICParams(); + } + viewport.say(chatmsg); // no await + } + } + } + + /** + * Handles an out-of-character chat message. + * @param {Array} args packet arguments + */ + handleCT(args) { + if (mode !== 'replay') { + const oocLog = document.getElementById('client_ooclog'); + oocLog.innerHTML += `${prepChat(args[1])}: ${prepChat(args[2])}\r\n`; + if (oocLog.scrollTop > oocLog.scrollHeight - 600) { + oocLog.scrollTop = oocLog.scrollHeight; + } + } + + } + + /** + * Handles a music change to an arbitrary resource. + * @param {Array} args packet arguments + */ + handleMC(args) { + const track = prepChat(args[1]); + let charID = Number(args[2]); + const showname = args[3] || ''; + const looping = Boolean(args[4]); + const channel = Number(args[5]) || 0; + // const fading = Number(args[6]) || 0; // unused in web + + const music = viewport.music[channel]; + let musicname; + music.pause(); + if (track.startsWith('http')) { + music.src = track; + } else { + music.src = `${AO_HOST}sounds/music/${encodeURI(track.toLowerCase())}`; + } + music.loop = looping; + music.play(); + + try { + musicname = this.chars[charID].name; + } catch (e) { + charID = -1; + } + + if (charID >= 0) { + musicname = this.chars[charID].name; + appendICLog(`${musicname} changed music to ${track}`); + } else { + appendICLog(`The music was changed to ${track}`); + } + + document.getElementById('client_trackstatustext').innerText = track; + } + + /** + * Handles a music change to an arbitrary resource, with an offset in seconds. + * @param {Array} args packet arguments + */ + handleRMC(args) { + viewport.music.pause(); + const { music } = viewport; + // Music offset + drift from song loading + music.totime = args[1]; + music.offset = new Date().getTime() / 1000; + music.addEventListener('loadedmetadata', () => { + music.currentTime += parseFloat(music.totime + (new Date().getTime() / 1000 - music.offset)).toFixed(3); + music.play(); + }, false); + } + + /** + * 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, charid) { + if (chargs[0]) { + let cini: any = {}; + const img = document.getElementById(`demo_${charid}`); + 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.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 = document.getElementById('mute_select'); + mute_select.add(new Option(safeTags(chargs[0]), charid)); + const pair_select = document.getElementById('pair_select'); + pair_select.add(new Option(safeTags(chargs[0]), 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.chat).toLowerCase() : safeTags(cini.options.category).toLowerCase(), + evidence: chargs[3], + icon: img.src, + inifile: cini, + muted: false, + }; + + if (this.chars[charid].blips === '') { this.chars[charid].blips = this.chars[charid].gender; } + + const iniedit_select = document.getElementById('client_ininame'); + iniedit_select.add(new Option(safeTags(chargs[0]))); + } else { + console.warn(`missing charid ${charid}`); + const img = document.getElementById(`demo_${charid}`); + img.style.display = 'none'; + } + } + + /** + * Handles incoming character information, bundling multiple characters + * per packet. + * CI#0#Phoenix&description&&&&#Miles ... + * @param {Array} args packet arguments + */ + handleCI(args) { + // Loop through the 10 characters that were sent + + for (let i = 2; i <= args.length - 2; i++) { + if (i % 2 === 0) { + document.getElementById('client_loadingtext').innerHTML = `Loading Character ${args[1]}/${this.char_list_length}`; + const chargs = args[i].split('&'); + const charid = args[i - 1]; + setTimeout(() => this.handleCharacterInfo(chargs, charid), 500); + } + } + // Request the next pack + this.sendServer(`AN#${(args[1] / 10) + 1}#%`); + } + + /** + * Handles incoming character information, containing all characters + * in one packet. + * @param {Array} args packet arguments + */ + async handleSC(args) { + const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + + // Add this so people can see characters loading on the screen. + document.getElementById('client_loading').style.display = 'none'; + document.getElementById('client_charselect').style.display = 'block'; + + document.getElementById('client_loadingtext').innerHTML = 'Loading Characters'; + for (let i = 1; i < args.length; i++) { + document.getElementById('client_loadingtext').innerHTML = `Loading Character ${i}/${this.char_list_length}`; + const chargs = args[i].split('&'); + const charid = i - 1; + await sleep(0.1); // TODO: Too many network calls without this. net::ERR_INSUFFICIENT_RESOURCES + this.handleCharacterInfo(chargs, charid); + } + // We're done with the characters, request the music + this.sendServer('RM#%'); + } + + /** + * Handles incoming evidence information, containing only one evidence + * item per packet. + * + * Mostly unimplemented in webAO. + * @param {Array} args packet arguments + */ + handleEI(args) { + document.getElementById('client_loadingtext').innerHTML = `Loading Evidence ${args[1]}/${this.evidence_list_length}`; + this.sendServer('RM#%'); + } + + /** + * Handles incoming evidence list, all evidences at once + * item per packet. + * + * @param {Array} args packet arguments + */ + handleLE(args) { + this.evidences = []; + for (let i = 1; i < args.length - 1; i++) { + const arg = args[i].split('&'); + this.evidences[i - 1] = { + name: prepChat(arg[0]), + desc: prepChat(arg[1]), + filename: safeTags(arg[2]), + icon: `${AO_HOST}evidence/${encodeURI(arg[2].toLowerCase())}`, + }; + } + + const evidence_box = document.getElementById('evidences'); + evidence_box.innerHTML = ''; + for (let i = 1; i <= this.evidences.length; i++) { + evidence_box.innerHTML += `${this.evidences[i - 1].name}`; + } + } + + resetMusicList() { + this.musics = []; + document.getElementById('client_musiclist').innerHTML = ''; + } + + resetAreaList() { + 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 = document.getElementById('bg_select'); + bg_select.innerHTML = ''; + + bg_select.add(new Option('Custom', '0')); + bg_array.forEach((background) => { + 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 = document.getElementById('client_ininame'); + char_select.innerHTML = ''; + + char_array.forEach((character) => { + 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 = document.getElementById('evi_select'); + evi_select.innerHTML = ''; + + evi_array.forEach((evi) => { + 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) { + const audioEndings = ['.wav', '.mp3', '.ogg', '.opus']; + return audioEndings.filter((ending) => trackname.endsWith(ending)).length === 1; + } + + addTrack(trackname) { + const newentry = document.createElement('OPTION'); + newentry.text = trackname; + (document.getElementById('client_musiclist')).options.add(newentry); + this.musics.push(trackname); + } + + createArea(id, name) { + 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(this); + }; + + 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 incoming music information, containing multiple entries + * per packet. + * @param {Array} args packet arguments + */ + handleEM(args) { + document.getElementById('client_loadingtext').innerHTML = 'Loading Music'; + if (args[1] === '0') { + this.resetMusicList(); + this.resetAreaList(); + this.musics_time = false; + } + + for (let i = 2; i < args.length - 1; i++) { + if (i % 2 === 0) { + document.getElementById('client_loadingtext').innerHTML = `Loading Music ${args[1]}/${this.music_list_length}`; + const trackname = safeTags(args[i]); + const trackindex = args[i - 1]; + if (this.musics_time) { + this.addTrack(trackname); + } else if (this.isAudio(trackname)) { + this.musics_time = true; + this.fix_last_area(); + this.addTrack(trackname); + } else { + this.createArea(trackindex, trackname); + } + } + } + + // get the next batch of tracks + this.sendServer(`AM#${(args[1] / 10) + 1}#%`); + } + + /** + * Handles incoming music information, containing all music in one packet. + * @param {Array} args packet arguments + */ + handleSM(args) { + document.getElementById('client_loadingtext').innerHTML = 'Loading Music '; + this.resetMusicList(); + this.resetAreaList(); + + this.musics_time = false; + + for (let i = 1; i < args.length - 1; i++) { + // Check when found the song for the first time + const trackname = safeTags(args[i]); + const trackindex = i - 1; + document.getElementById('client_loadingtext').innerHTML = `Loading Music ${i}/${this.music_list_length}`; + if (this.musics_time) { + this.addTrack(trackname); + } else if (this.isAudio(trackname)) { + this.musics_time = true; + this.fix_last_area(); + this.addTrack(trackname); + } else { + this.createArea(trackindex, trackname); + } + } + + // Music done, carry on + this.sendServer('RD#%'); + } + + /** + * Handles updated music list + * @param {Array} args packet arguments + */ + handleFM(args) { + this.resetMusicList(); + + for (let i = 1; i < args.length - 1; i++) { + // Check when found the song for the first time + this.addTrack(safeTags(args[i])); + } + } + + /** + * Handles updated area list + * @param {Array} args packet arguments + */ + handleFA(args) { + this.resetAreaList(); + + for (let i = 1; i < args.length - 1; i++) { + this.createArea(i - 1, safeTags(args[i])); + } + } + + /** + * Handles the "MusicMode" packet + * @param {Array} args packet arguments + */ + handleMM(_args) { + // It's unused nowadays, as preventing people from changing the music is now serverside + } + + /** + * Handles the kicked packet + * @param {string} type is it a kick or a ban + * @param {string} reason why + */ + handleBans(type, reason) { + document.getElementById('client_error').style.display = 'flex'; + document.getElementById('client_errortext').innerHTML = `${type}:
${reason.replace(/\n/g, '
')}`; + (document.getElementsByClassName('client_reconnect')[0]).style.display = 'none'; + (document.getElementsByClassName('client_reconnect')[1]).style.display = 'none'; + } + + /** + * Handles the kicked packet + * @param {Array} args kick reason + */ + handleKK(args) { + this.handleBans('Kicked', safeTags(args[1])); + } + + /** + * Handles the banned packet + * this one is sent when you are kicked off the server + * @param {Array} args ban reason + */ + handleKB(args) { + this.handleBans('Banned', safeTags(args[1])); + this.banned = true; + } + + /** + * Handles the warning packet + * on client this spawns a message box you can't close for 2 seconds + * @param {Array} args ban reason + */ + handleBB(args) { + alert(safeTags(args[1])); + } + + /** + * Handles the banned packet + * this one is sent when you try to reconnect but you're banned + * @param {Array} args ban reason + */ + handleBD(args) { + this.handleBans('Banned', safeTags(args[1])); + this.banned = true; + } + + /** + * Handles the handshake completion packet, meaning the player + * is ready to select a character. + * + * @param {Array} args packet arguments + */ + handleDONE(_args) { + document.getElementById('client_loading').style.display = 'none'; + if (mode === 'watch') { // Spectators don't need to pick a character + document.getElementById('client_charselect').style.display = 'none'; + } + } + + /** + * Handles a background change. + * @param {Array} args packet arguments + */ + + handleBN(args) { + viewport.bgname = safeTags(args[1]); + const bgfolder = viewport.bgFolder; + const bg_index = getIndexFromSelect('bg_select', viewport.bgname); + (document.getElementById('bg_select')).selectedIndex = bg_index; + updateBackgroundPreview(); + if (bg_index === 0) { + (document.getElementById('bg_filename')).value = viewport.bgname; + } + + tryUrls(`${AO_HOST}background/${encodeURI(args[1].toLowerCase())}/defenseempty`).then(resp => {(document.getElementById('bg_preview')).src = resp}); + tryUrls(`${bgfolder}defensedesk`).then((resp) => {(document.getElementById('client_def_bench')).src = resp}); + tryUrls(`${bgfolder}stand`).then(resp => {(document.getElementById('client_wit_bench')).src = resp}); + tryUrls(`${bgfolder}prosecutiondesk`).then(resp => {(document.getElementById('client_pro_bench')).src = resp}); + tryUrls(`${bgfolder}full`).then(resp => {(document.getElementById('client_court')).src = resp}); + tryUrls(`${bgfolder}defenseempty`).then(resp => {(document.getElementById('client_court_def')).src = resp}); + tryUrls(`${bgfolder}transition_def`).then(resp => {(document.getElementById('client_court_deft')).src = resp}); + tryUrls(`${bgfolder}witnessempty`).then(resp => {(document.getElementById('client_court_wit')).src = resp}); + tryUrls(`${bgfolder}transition_pro`).then(resp => {(document.getElementById('client_court_prot')).src = resp}); + tryUrls(`${bgfolder}prosecutorempty`).then(resp => {(document.getElementById('client_court_pro')).src = resp}); + + if (this.charID === -1) { + viewport.changeBackground('jud'); + } else { + viewport.changeBackground(this.chars[this.charID].side); + } + } + + /** + * Handles a change in the health bars' states. + * @param {Array} args packet arguments + */ + handleHP(args) { + const percent_hp = Number(args[2]) * 10; + let healthbox; + if (args[1] === '1') { + // Def hp + this.hp[0] = args[2]; + healthbox = document.getElementById('client_defense_hp'); + } else { + // Pro hp + this.hp[1] = args[2]; + healthbox = document.getElementById('client_prosecutor_hp'); + } + healthbox.getElementsByClassName('health-bar')[0].style.width = `${percent_hp}%`; + } + + /** + * Handles a testimony states. + * @param {Array} args packet arguments + */ + handleRT(args) { + const judgeid = Number(args[2]); + switch (args[1]) { + case 'testimony1': + this.testimonyID = 1; + break; + case 'testimony2': + // Cross Examination + this.testimonyID = 2; + break; + case 'judgeruling': + this.testimonyID = 3 + judgeid; + break; + default: + console.warn('Invalid testimony'); + } + viewport.initTestimonyUpdater(); + } + + /** + * Handles a timer update + * @param {Array} args packet arguments + */ + handleTI(args) { + const timerid = Number(args[1]); + const type = Number(args[2]); + const timer_value = args[3]; + switch (type) { + case 0: + // + case 1: + document.getElementById(`client_timer${timerid}`).innerText = timer_value; + case 2: + document.getElementById(`client_timer${timerid}`).style.display = ''; + case 3: + document.getElementById(`client_timer${timerid}`).style.display = 'none'; + } + } + + /** + * Handles a modcall + * @param {Array} args packet arguments + */ + handleZZ(args) { + const oocLog = document.getElementById('client_ooclog'); + oocLog.innerHTML += `$Alert: ${prepChat(args[1])}\r\n`; + if (oocLog.scrollTop > oocLog.scrollHeight - 60) { + oocLog.scrollTop = oocLog.scrollHeight; + } + viewport.sfxaudio.pause(); + const oldvolume = viewport.sfxaudio.volume; + viewport.sfxaudio.volume = 1; + viewport.sfxaudio.src = `${AO_HOST}sounds/general/sfx-gallery.opus`; + viewport.sfxaudio.play(); + viewport.sfxaudio.volume = oldvolume; + } + + /** + * Handle the player + * @param {Array} args packet arguments + */ + handleHI(args) { + this.sendSelf(`ID#1#webAO#${version}#%`); + this.sendSelf('FL#fastloading#yellowtext#cccc_ic_support#flipping#looping_sfx#effects#%'); + } + + /** + * Identifies the server and issues a playerID + * @param {Array} args packet arguments + */ + handleID(args) { + this.playerID = Number(args[1]); + const serverSoftware = args[2].split('&')[0]; + let serverVersion; + if (serverSoftware === 'serverD') { + serverVersion = args[2].split('&')[1]; + } else if (serverSoftware === 'webAO') { + oldLoading = false; + this.sendSelf('PN#0#1#%'); + } else { + serverVersion = args[3]; + } + + if (serverSoftware === 'serverD' && serverVersion === '1377.152') { oldLoading = true; } // bugged version + } + + /** + * Indicates how many users are on this server + * @param {Array} args packet arguments + */ + handlePN(_args) { + this.sendServer('askchaa#%'); + } + + /** + * What? you want a character?? + * @param {Array} args packet arguments + */ + handleCC(args) { + this.sendSelf(`PV#1#CID#${args[2]}#%`); + } + + /** + * What? you want a character list from me?? + * @param {Array} args packet arguments + */ + handleaskchaa(_args) { + this.sendSelf(`SI#${vanilla_character_arr.length}#0#0#%`); + } + + /** + * Handle the change of players in an area. + * @param {Array} args packet arguments + */ + handleARUP(args) { + args = args.slice(1); + for (let i = 0; i < args.length - 2; i++) { + if (this.areas[i]) { // the server sends us ARUP before we even get the area list + const thisarea = document.getElementById(`area${i}`); + switch (Number(args[0])) { + case 0: // playercount + this.areas[i].players = Number(args[i + 1]); + break; + case 1: // status + this.areas[i].status = safeTags(args[i + 1]); + break; + case 2: + this.areas[i].cm = safeTags(args[i + 1]); + break; + case 3: + this.areas[i].locked = safeTags(args[i + 1]); + break; + } + + thisarea.className = `area-button area-${this.areas[i].status.toLowerCase()}`; + + thisarea.innerText = `${this.areas[i].name} (${this.areas[i].players}) [${this.areas[i].status}]`; + + thisarea.title = `Players: ${this.areas[i].players}\n` + + `Status: ${this.areas[i].status}\n` + + `CM: ${this.areas[i].cm}\n` + + `Area lock: ${this.areas[i].locked}`; + } + } + } + + /** + * With this the server tells us which features it supports + * @param {Array} args list of features + */ + handleFL(args) { + console.info('Server-supported features:'); + console.info(args); + extrafeatures = args; + + if (args.includes('yellowtext')) { + const colorselect = 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'); + } + + if (args.includes('cccc_ic_support')) { + document.getElementById('cccc').style.display = ''; + document.getElementById('pairing').style.display = ''; + } + + if (args.includes('flipping')) { + document.getElementById('button_flip').style.display = ''; + } + + if (args.includes('looping_sfx')) { + document.getElementById('button_shake').style.display = ''; + document.getElementById('2.7').style.display = ''; + } + + if (args.includes('effects')) { + document.getElementById('2.8').style.display = ''; + } + + if (args.includes('y_offset')) { + document.getElementById('y_offset').style.display = ''; + } + } + + /** + * Received when the server announces its server info, + * but we use it as a cue to begin retrieving characters. + * @param {Array} args packet arguments + */ + handleSI(args) { + this.char_list_length = Number(args[1]); + this.char_list_length += 1; // some servers count starting from 0 some from 1... + this.evidence_list_length = Number(args[2]); + this.music_list_length = Number(args[3]); + + // create the charselect grid, to be filled by the character loader + document.getElementById('client_chartable').innerHTML = ''; + + for (let i = 0; i < this.char_list_length; i++) { + const demothing = document.createElement('img'); + + demothing.className = 'demothing'; + demothing.id = `demo_${i}`; + const demoonclick = document.createAttribute('onclick'); + demoonclick.value = `pickChar(${i})`; + demothing.setAttributeNode(demoonclick); + + document.getElementById('client_chartable').appendChild(demothing); + } + + // this is determined at the top of this file + if (!oldLoading && extrafeatures.includes('fastloading')) { + this.sendServer('RC#%'); + } else { + this.sendServer('askchar2#%'); + } + } + + /** + * Handles the list of all used and vacant characters. + * @param {Array} args list of all characters represented as a 0 for free or a -1 for taken + */ + handleCharsCheck(args) { + for (let i = 0; i < this.char_list_length; i++) { + const img = document.getElementById(`demo_${i}`); + + if (args[i + 1] === '-1') { img.style.opacity = '0.25'; } else if (args[i + 1] === '0') { img.style.opacity = '1'; } + } + } + + /** + * Handles the server's assignment of a character for the player to use. + * PV # playerID (unused) # CID # character ID + * @param {Array} args packet arguments + */ + async handlePV(args) { + this.charID = Number(args[3]); + document.getElementById('client_charselect').style.display = 'none'; + + const me = this.chars[this.charID]; + this.selectedEmote = -1; + const { emotes } = this; + const emotesList = document.getElementById('client_emo'); + emotesList.style.display = ''; + emotesList.innerHTML = ''; // Clear emote box + const ini = me.inifile; + me.side = ini.options.side; + updateActionCommands(me.side); + if (ini.emotions.number === 0) { + emotesList.innerHTML = `No emotes available`; + } else { + for (let i = 1; i <= ini.emotions.number; i++) { + try { + const emoteinfo = ini.emotions[i].split('#'); + let esfx; + let esfxd; + try { + esfx = ini.soundn[i] || '0'; + esfxd = Number(ini.soundt[i]) || 0; + } catch (e) { + console.warn('ini sound is completly missing'); + esfx = '0'; + esfxd = 0; + } + // Make sure the asset server is case insensitive, or that everything on it is lowercase + + emotes[i] = { + desc: emoteinfo[0].toLowerCase(), + preanim: emoteinfo[1].toLowerCase(), + emote: emoteinfo[2].toLowerCase(), + zoom: Number(emoteinfo[3]) || 0, + sfx: esfx.toLowerCase(), + sfxdelay: esfxd, + frame_screenshake: '', + frame_realization: '', + frame_sfx: '', + button: `${AO_HOST}characters/${encodeURI(me.name.toLowerCase())}/emotions/button${i}_off.png`, + }; + emotesList.innerHTML + += `${emotes[i].desc}`; + } catch (e) { + console.error(`missing emote ${i}`); + } + } + pickEmotion(1); + } + + if (await fileExists(`${AO_HOST}characters/${encodeURI(me.name.toLowerCase())}/custom.gif`)) { document.getElementById('button_4').style.display = ''; } else { document.getElementById('button_4').style.display = 'none'; } + + const iniedit_select = document.getElementById('client_ininame'); + + // Load iniswaps if there are any + try { + const cswapdata = await request(`${AO_HOST}characters/${encodeURI(me.name.toLowerCase())}/iniswaps.ini`); + const cswap = cswapdata.split('\n'); + + // most iniswaps don't list their original char + if (cswap.length > 0) { + iniedit_select.innerHTML = ''; + + iniedit_select.add(new Option(safeTags(me.name))); + + cswap.forEach((inisw) => iniedit_select.add(new Option(safeTags(inisw)))); + } + } catch (err) { + console.info("character doesn't have iniswaps"); + this.fetchCharacterList(); + } + } + + /** + * new asset url!! + * @param {Array} args packet arguments + */ + handleASS(args) { + AO_HOST = args[1]; + } + + /** + * we are asking ourselves what characters there are + * @param {Array} args packet arguments + */ + handleRC(_args) { + this.sendSelf(`SC#${vanilla_character_arr.join('#')}#%`); + } + + /** + * we are asking ourselves what characters there are + * @param {Array} args packet arguments + */ + handleRM(_args) { + this.sendSelf(`SM#${vanilla_music_arr.join('#')}#%`); + } + + /** + * we are asking ourselves what characters there are + * @param {Array} args packet arguments + */ + handleRD(_args) { + this.sendSelf('BN#gs4#%'); + this.sendSelf('DONE#%'); + const ooclog = document.getElementById('client_ooclog'); + ooclog.value = ''; + ooclog.readOnly = false; + + document.getElementById('client_oocinput').style.display = 'none'; + document.getElementById('client_replaycontrols').style.display = 'inline-block'; + } +} + +class Viewport { + textnow: string; + chatmsg: any; + shouts: string[]; + colors: string[]; + blipChannels: any; + currentBlipChannel: number; + sfxaudio: any; + sfxplayed: number; + shoutaudio: any; + testimonyAudio: any; + music: any; + updater: any; + testimonyUpdater: any; + bgname: string; + lastChar: string; + lastEvi: number; + testimonyTimer: number; + shoutTimer: number; + tickTimer: number; + _animating: boolean; + startFirstTickCheck: boolean; + startSecondTickCheck: boolean; + startThirdTickCheck: boolean; + + constructor() { + this.textnow = ''; + this.chatmsg = { + content: '', + objection: 0, + sound: '', + startpreanim: true, + startspeaking: false, + side: null, + color: 0, + snddelay: 0, + preanimdelay: 0, + }; + + this.shouts = [ + undefined, + 'holdit', + 'objection', + 'takethat', + 'custom', + ]; + + this.colors = [ + 'white', + 'green', + 'red', + 'orange', + 'blue', + 'yellow', + 'pink', + 'cyan', + 'grey', + ]; + + // Allocate multiple blip audio channels to make blips less jittery + const blipSelectors = document.getElementsByClassName('blipSound') + this.blipChannels = [...blipSelectors]; + this.blipChannels.forEach((channel) => channel.volume = 0.5); + this.blipChannels.forEach((channel) => channel.onerror = opusCheck(channel)); + this.currentBlipChannel = 0; + + this.sfxaudio = document.getElementById('client_sfxaudio'); + this.sfxaudio.src = `${AO_HOST}sounds/general/sfx-realization.opus`; + + this.sfxplayed = 0; + + this.shoutaudio = document.getElementById('client_shoutaudio'); + this.shoutaudio.src = `${AO_HOST}misc/default/objection.opus`; + + this.testimonyAudio = document.getElementById('client_testimonyaudio'); + this.testimonyAudio.src = `${AO_HOST}sounds/general/sfx-guilty.opus`; + + const audioChannels = document.getElementsByClassName('audioChannel') + this.music = [...audioChannels]; + this.music.forEach((channel) => channel.volume = 0.5); + this.music.forEach((channel) => channel.onerror = opusCheck(channel)); + + this.updater = null; + this.testimonyUpdater = null; + + this.bgname = 'gs4'; + + this.lastChar = ''; + this.lastEvi = 0; + + this.testimonyTimer = 0; + this.shoutTimer = 0; + this.tickTimer = 0; + + this._animating = false; + } + + /** + * Sets the volume of the music. + * @param {number} volume + */ + set musicVolume(volume) { + this.music.forEach((channel) => channel.volume = volume); + } + + /** + * Returns the path which the background is located in. + */ + get bgFolder() { + return `${AO_HOST}background/${encodeURI(this.bgname.toLowerCase())}/`; + } + + /** + * Play any SFX + * + * @param {string} sfxname + */ + async playSFX(sfxname, looping) { + this.sfxaudio.pause(); + this.sfxaudio.loop = looping; + this.sfxaudio.src = sfxname; + this.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 + */ + async changeBackground(position) { + const bgfolder = viewport.bgFolder; + + const view = document.getElementById('client_fullview'); + + let bench; + if ('def,pro,wit'.includes(position)) { + bench = document.getElementById(`client_${position}_bench`); + } else { + bench = document.getElementById('client_bench_classic'); + } + + let court; + if ('def,pro,wit'.includes(position)) { + court = document.getElementById(`client_court_${position}`); + } else { + court = document.getElementById('client_court_classic'); + } + + const positions = { + def: { + bg: 'defenseempty', + desk: { ao2: 'defensedesk.png', ao1: 'bancodefensa.png' }, + speedLines: 'defense_speedlines.gif', + }, + pro: { + bg: 'prosecutorempty', + desk: { ao2: 'prosecutiondesk.png', ao1: 'bancoacusacion.png' }, + speedLines: 'prosecution_speedlines.gif', + }, + hld: { + bg: 'helperstand', + desk: null, + speedLines: 'defense_speedlines.gif', + }, + hlp: { + bg: 'prohelperstand', + desk: null, + speedLines: 'prosecution_speedlines.gif', + }, + wit: { + bg: 'witnessempty', + desk: { ao2: 'stand.png', ao1: 'estrado.png' }, + speedLines: 'prosecution_speedlines.gif', + }, + jud: { + bg: 'judgestand', + desk: { ao2: 'judgedesk.png', ao1: 'judgedesk.gif' }, + speedLines: 'prosecution_speedlines.gif', + }, + jur: { + bg: 'jurystand', + desk: { ao2: 'jurydesk.png', ao1: 'estrado.png' }, + speedLines: 'defense_speedlines.gif', + }, + sea: { + bg: 'seancestand', + desk: { ao2: 'seancedesk.png', ao1: 'estrado.png' }, + speedLines: 'prosecution_speedlines.gif', + }, + }; + + 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 (viewport.chatmsg.type === 5) { + console.warn('this is a zoom'); + court.src = `${AO_HOST}themes/default/${encodeURI(speedLines)}`; + bench.style.opacity = '0'; + } else { + // Set src here + + court.src = await tryUrls(bgfolder + bg) + if (desk) { + const deskFilename = await fileExists(bgfolder + desk.ao2) ? desk.ao2 : desk.ao1; + bench.src = bgfolder + 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 + */ + initTestimonyUpdater() { + const testimonyFilenames = { + 1: 'witnesstestimony', + 2: 'crossexamination', + 3: 'notguilty', + 4: 'guilty', + }; + + const testimony = testimonyFilenames[client.testimonyID]; + if (!testimony) { + console.warn(`Invalid testimony ID ${client.testimonyID}`); + return; + } + + this.testimonyAudio.src = client.resources[testimony].sfx; + this.testimonyAudio.play(); + + const testimonyOverlay = document.getElementById('client_testimony'); + testimonyOverlay.src = client.resources[testimony].src; + testimonyOverlay.style.opacity = '1'; + + this.testimonyTimer = 0; + this.testimonyUpdater = setTimeout(() => this.updateTestimony(), UPDATE_INTERVAL); + } + + /** + * Updates the testimony overaly + */ + updateTestimony() { + const testimonyFilenames = { + 1: 'witnesstestimony', + 2: 'crossexamination', + 3: 'notguilty', + 4: 'guilty', + }; + + // Update timer + this.testimonyTimer += UPDATE_INTERVAL; + + const testimony = testimonyFilenames[client.testimonyID]; + const resource = client.resources[testimony]; + if (!resource) { + this.disposeTestimony(); + return; + } + + if (this.testimonyTimer >= resource.duration) { + this.disposeTestimony(); + } else { + this.testimonyUpdater = setTimeout(() => this.updateTestimony(), UPDATE_INTERVAL); + } + } + + /** + * Dispose the testimony overlay + */ + disposeTestimony() { + client.testimonyID = 0; + this.testimonyTimer = 0; + document.getElementById('client_testimony').style.opacity = '0'; + clearTimeout(this.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 + */ + async say(chatmsg) { + + this.chatmsg = chatmsg; + this.textnow = ''; + this.sfxplayed = 0; + this.tickTimer = 0; + this._animating = true; + this.startFirstTickCheck = true + this.startSecondTickCheck = false + this.startThirdTickCheck = false + let charLayers = document.getElementById('client_char'); + let pairLayers = document.getElementById('client_pair_char'); + + // stop updater + clearTimeout(this.updater); + + // stop last sfx from looping any longer + this.sfxaudio.loop = false; + + const fg = document.getElementById('client_fg'); + const gamewindow = document.getElementById('client_gamewindow'); + const waitingBox = document.getElementById('client_chatwaiting'); + + // Reset CSS animation + gamewindow.style.animation = ''; + waitingBox.style.opacity = '0'; + + const eviBox = document.getElementById('client_evi'); + + if (this.lastEvi !== this.chatmsg.evidence) { + eviBox.style.opacity = '0'; + eviBox.style.height = '0%'; + } + this.lastEvi = this.chatmsg.evidence; + + const validSides = ['def', 'pro', 'wit']; + if (validSides.includes(this.chatmsg.side)) { + charLayers = document.getElementById(`client_${this.chatmsg.side}_char`); + pairLayers = document.getElementById(`client_${this.chatmsg.side}_pair_char`); + } + + const chatContainerBox = document.getElementById('client_chatcontainer'); + const nameBoxInner = document.getElementById('client_inner_name'); + const chatBoxInner = document.getElementById('client_inner_chat'); + + const displayname = ((document.getElementById('showname')).checked && this.chatmsg.showname !== '') ? this.chatmsg.showname : this.chatmsg.nameplate; + + // Clear out the last message + chatBoxInner.innerText = this.textnow; + nameBoxInner.innerText = displayname; + + if (this.lastChar !== this.chatmsg.name) { + charLayers.style.opacity = '0'; + pairLayers.style.opacity = '0'; + } + this.lastChar = this.chatmsg.name; + + appendICLog(this.chatmsg.content, this.chatmsg.showname, this.chatmsg.nameplate); + + checkCallword(this.chatmsg.content); + + setEmote(AO_HOST, this, this.chatmsg.name.toLowerCase(), this.chatmsg.sprite, '(a)', false, this.chatmsg.side); + + if (this.chatmsg.other_name) { + setEmote(AO_HOST, this, this.chatmsg.other_name.toLowerCase(), this.chatmsg.other_emote, '(a)', false, this.chatmsg.side); + } + + // gets which shout shall played + const shoutSprite = document.getElementById('client_shout'); + const shout = this.shouts[this.chatmsg.objection]; + if (shout) { + // Hide message box + chatContainerBox.style.opacity = '0'; + if (this.chatmsg.objection === 4) { + shoutSprite.src = `${AO_HOST}characters/${encodeURI(this.chatmsg.name.toLowerCase())}/custom.gif`; + } else { + shoutSprite.src = client.resources[shout].src; + shoutSprite.style.animation = 'bubble 700ms steps(10, jump-both)'; + } + shoutSprite.style.opacity = '1'; + + this.shoutaudio.src = `${AO_HOST}characters/${encodeURI(this.chatmsg.name.toLowerCase())}/${shout}.opus`; + this.shoutaudio.play(); + this.shoutTimer = client.resources[shout].duration; + } else { + this.shoutTimer = 0; + } + + this.chatmsg.startpreanim = true; + let gifLength = 0; + + if (this.chatmsg.type === 1 && this.chatmsg.preanim !== '-') { + chatContainerBox.style.opacity = '0'; + gifLength = await getAnimLength(`${AO_HOST}characters/${encodeURI(this.chatmsg.name.toLowerCase())}/${encodeURI(this.chatmsg.preanim)}`); + this.chatmsg.startspeaking = false; + } else { + this.chatmsg.startspeaking = true; + } + this.chatmsg.preanimdelay = gifLength; + + this.changeBackground(chatmsg.side); + + setChatbox(chatmsg.chatbox); + resizeChatbox(); + + // Flip the character + charLayers.style.transform = this.chatmsg.flip === 1 ? 'scaleX(-1)' : 'scaleX(1)'; + + // Shift by the horizontal offset + switch (this.chatmsg.side) { + case 'wit': + pairLayers.style.left = `${200 + Number(this.chatmsg.other_offset[0])}%`; + charLayers.style.left = `${200 + Number(this.chatmsg.self_offset[0])}%`; + break; + case 'pro': + pairLayers.style.left = `${400 + Number(this.chatmsg.other_offset[0])}%`; + charLayers.style.left = `${400 + Number(this.chatmsg.self_offset[0])}%`; + break; + default: + pairLayers.style.left = `${Number(this.chatmsg.other_offset[0])}%`; + charLayers.style.left = `${Number(this.chatmsg.self_offset[0])}%`; + break; + } + + // New vertical offsets + pairLayers.style.top = `${Number(this.chatmsg.other_offset[1])}%`; + charLayers.style.top = `${Number(this.chatmsg.self_offset[1])}%`; + + // flip the paired character + pairLayers.style.transform = this.chatmsg.other_flip === 1 ? 'scaleX(-1)' : 'scaleX(1)'; + + this.blipChannels.forEach((channel) => channel.src = `${AO_HOST}sounds/general/sfx-blip${encodeURI(this.chatmsg.blips.toLowerCase())}.opus`); + + // process markup + if (this.chatmsg.content.startsWith('~~')) { + chatBoxInner.style.textAlign = 'center'; + this.chatmsg.content = this.chatmsg.content.substring(2, this.chatmsg.content.length); + } else { + chatBoxInner.style.textAlign = 'inherit'; + } + + // apply effects + fg.style.animation = ''; + const badEffects = ['', '-', 'none']; + if (this.chatmsg.effects[0] && !badEffects.includes(this.chatmsg.effects[0].toLowerCase())) { + const baseEffectUrl = `${AO_HOST}themes/default/effects/`; + fg.src = `${baseEffectUrl}${encodeURI(this.chatmsg.effects[0].toLowerCase())}.webp`; + } else { + + fg.src = transparentPng; + } + + const soundChecks = ['0', '1', '', undefined]; + if (soundChecks.some((check) => this.chatmsg.sound === check)) { + this.chatmsg.sound = this.chatmsg.effects[2]; + } + this.chatmsg.parsed = await attorneyMarkdown.applyMarkdown(chatmsg.content, this.colors[this.chatmsg.color]) + this.tick(); + } + + handleTextTick(charLayers) { + const chatBox = document.getElementById('client_chat'); + const waitingBox = document.getElementById('client_chatwaiting'); + const chatBoxInner = document.getElementById('client_inner_chat'); + const charName = this.chatmsg.name.toLowerCase(); + const charEmote = this.chatmsg.sprite.toLowerCase(); + + + if (this.chatmsg.content.charAt(this.textnow.length) !== ' ') { + this.blipChannels[this.currentBlipChannel].play(); + this.currentBlipChannel++; + this.currentBlipChannel %= this.blipChannels.length; + } + this.textnow = this.chatmsg.content.substring(0, this.textnow.length + 1); + const characterElement = this.chatmsg.parsed[this.textnow.length - 1] + if (characterElement) { + chatBoxInner.appendChild(this.chatmsg.parsed[this.textnow.length - 1]); + } + + // scroll to bottom + chatBox.scrollTop = chatBox.scrollHeight; + + if (this.textnow === this.chatmsg.content) { + this._animating = false; + setEmote(AO_HOST, this, charName, charEmote, '(a)', false, this.chatmsg.side); + charLayers.style.opacity = '1'; + waitingBox.style.opacity = '1'; + clearTimeout(this.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 `this.chatmsg`! + */ + tick() { + if (this._animating) { + this.updater = setTimeout(() => this.tick(), UPDATE_INTERVAL); + } + + const gamewindow = document.getElementById('client_gamewindow'); + const waitingBox = document.getElementById('client_chatwaiting'); + const eviBox = document.getElementById('client_evi'); + const shoutSprite = document.getElementById('client_shout'); + const effectlayer = document.getElementById('client_fg'); + const chatBoxInner = document.getElementById('client_inner_chat'); + let charLayers = document.getElementById('client_char'); + let pairLayers = document.getElementById('client_pair_char'); + + if ('def,pro,wit'.includes(this.chatmsg.side)) { + charLayers = document.getElementById(`client_${this.chatmsg.side}_char`); + pairLayers = document.getElementById(`client_${this.chatmsg.side}_pair_char`); + } + + const charName = this.chatmsg.name.toLowerCase(); + const charEmote = this.chatmsg.sprite.toLowerCase(); + + const pairName = this.chatmsg.other_name.toLowerCase(); + const pairEmote = this.chatmsg.other_emote.toLowerCase(); + + // TODO: preanims sometimes play when they're not supposed to + const isShoutOver = this.tickTimer >= this.shoutTimer + const isShoutAndPreanimOver = this.tickTimer >= this.shoutTimer + this.chatmsg.preanimdelay + if (isShoutOver && this.startFirstTickCheck) { + // Effect stuff + if (this.chatmsg.screenshake === 1) { + // Shake screen + this.playSFX(`${AO_HOST}sounds/general/sfx-stab.opus`, false); + gamewindow.style.animation = 'shake 0.2s 1'; + } + if (this.chatmsg.flash === 1) { + // Flash screen + this.playSFX(`${AO_HOST}sounds/general/sfx-realization.opus`, false); + effectlayer.style.animation = 'flash 0.4s 1'; + } + + // Pre-animation stuff + if (this.chatmsg.preanimdelay > 0) { + shoutSprite.style.opacity = '0'; + shoutSprite.style.animation = ''; + const preanim = this.chatmsg.preanim.toLowerCase(); + setEmote(AO_HOST, this, charName, preanim, '', false, this.chatmsg.side); + charLayers.style.opacity = '1'; + } + + if (this.chatmsg.other_name) { + pairLayers.style.opacity = '1'; + } else { + pairLayers.style.opacity = '0'; + } + // Done with first check, move to second + this.startFirstTickCheck = false + this.startSecondTickCheck = true + + this.chatmsg.startpreanim = false; + this.chatmsg.startspeaking = true; + } + const hasNonInterruptingPreAnim = this.chatmsg.noninterrupting_preanim === 1 + if (this.textnow !== this.chatmsg.content && hasNonInterruptingPreAnim) { + const chatContainerBox = document.getElementById('client_chatcontainer'); + chatContainerBox.style.opacity = '1'; + this.handleTextTick(charLayers) + + }else if (isShoutAndPreanimOver && this.startSecondTickCheck) { + if (this.chatmsg.startspeaking) { + this.chatmsg.startspeaking = false; + + // Evidence Bullshit + if (this.chatmsg.evidence > 0) { + // Prepare evidence + eviBox.src = safeTags(client.evidences[this.chatmsg.evidence - 1].icon); + + eviBox.style.width = 'auto'; + eviBox.style.height = '36.5%'; + eviBox.style.opacity = '1'; + + this.testimonyAudio.src = `${AO_HOST}sounds/general/sfx-evidenceshoop.opus`; + this.testimonyAudio.play(); + + if (this.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_${this.colors[this.chatmsg.color]}`; + + + if (this.chatmsg.preanimdelay === 0) { + shoutSprite.style.opacity = '0'; + shoutSprite.style.animation = ''; + } + + if (this.chatmsg.other_name) { + setEmote(AO_HOST, this, pairName, pairEmote, '(a)', true, this.chatmsg.side); + pairLayers.style.opacity = '1'; + } else { + pairLayers.style.opacity = '0'; + } + + setEmote(AO_HOST, this, charName, charEmote, '(b)', false, this.chatmsg.side); + charLayers.style.opacity = '1'; + + if (this.textnow === this.chatmsg.content) { + setEmote(AO_HOST, this, charName, charEmote, '(a)', false, this.chatmsg.side); + charLayers.style.opacity = '1'; + waitingBox.style.opacity = '1'; + this._animating = false; + clearTimeout(this.updater); + } + } else if (this.textnow !== this.chatmsg.content) { + this.handleTextTick(charLayers) + } + } + + if (!this.sfxplayed && this.chatmsg.snddelay + this.shoutTimer >= this.tickTimer) { + this.sfxplayed = 1; + if (this.chatmsg.sound !== '0' && this.chatmsg.sound !== '1' && this.chatmsg.sound !== '' && this.chatmsg.sound !== undefined && (this.chatmsg.type == 1 || this.chatmsg.type == 2 || this.chatmsg.type == 6)) { + this.playSFX(`${AO_HOST}sounds/general/${encodeURI(this.chatmsg.sound.toLowerCase())}.opus`, this.chatmsg.looping_sfx); + } + } + this.tickTimer += UPDATE_INTERVAL; + } +} + +/** + * Triggered when the Return key is pressed on the out-of-character chat input box. + * @param {KeyboardEvent} event + */ +export function onOOCEnter(event) { + if (event.keyCode === 13) { + client.sendOOC((document.getElementById('client_oocinputbox')).value); + (document.getElementById('client_oocinputbox')).value = ''; + } +} +window.onOOCEnter = onOOCEnter; + +/** + * Triggered when the user click replay GOOOOO + * @param {KeyboardEvent} event + */ +export function onReplayGo(_event) { + client.handleReplay(); +} +window.onReplayGo = onReplayGo; + +/** + * Triggered when the Return key is pressed on the in-character chat input box. + * @param {KeyboardEvent} event + */ +export function onEnter(event) { + if (event.keyCode === 13) { + const mychar = client.character; + const myemo = client.emote; + const evi = client.evidence; + const flip = ((document.getElementById('button_flip').classList.contains('dark')) ? 1 : 0); + const flash = ((document.getElementById('button_flash').classList.contains('dark')) ? 1 : 0); + const screenshake = ((document.getElementById('button_shake').classList.contains('dark')) ? 1 : 0); + const noninterrupting_preanim = (((document.getElementById('check_nonint')).checked) ? 1 : 0); + const looping_sfx = (((document.getElementById('check_loopsfx')).checked) ? 1 : 0); + const color = (document.getElementById('textcolor')).value; + const showname = (document.getElementById('ic_chat_name')).value; + const text = (document.getElementById('client_inputbox')).value; + const pairchar = (document.getElementById('pair_select')).value; + const pairoffset = (document.getElementById('pair_offset')).value; + const pairyoffset = (document.getElementById('pair_y_offset')).value; + const myrole = (document.getElementById('role_select')).value ? (document.getElementById('role_select')).value : mychar.side; + const additive = (((document.getElementById('check_additive')).checked) ? 1 : 0); + const effect = (document.getElementById('effect_select')).value; + + let sfxname = '0'; + let sfxdelay = 0; + let emote_mod = myemo.zoom; + if ((document.getElementById('sendsfx')).checked) { + sfxname = myemo.sfx; + sfxdelay = myemo.sfxdelay; + } + + // not to overwrite a 5 from the ini or anything else + if ((document.getElementById('sendpreanim')).checked) { + if (emote_mod === 0) { emote_mod = 1; } + } else if (emote_mod === 1) { emote_mod = 0; } + + + client.sendIC( + 'chat', + myemo.preanim, + mychar.name, + myemo.emote, + text, + myrole, + sfxname, + emote_mod, + sfxdelay, + selectedShout, + evi, + flip, + flash, + color, + showname, + pairchar, + pairoffset, + pairyoffset, + noninterrupting_preanim, + looping_sfx, + screenshake, + '-', + '-', + '-', + additive, + effect, + ); + } +} +window.onEnter = onEnter; + +/** + * 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. + */ +function resetICParams() { + (document.getElementById('client_inputbox')).value = ''; + document.getElementById('button_flash').className = 'client_button'; + document.getElementById('button_shake').className = 'client_button'; + + (document.getElementById('sendpreanim')).checked = false; + + if (selectedShout) { + document.getElementById(`button_${selectedShout}`).className = 'client_button'; + selectedShout = 0; + } +} + +export function resetOffset(_event) { + (document.getElementById('pair_offset')).value = '0'; + (document.getElementById('pair_y_offset')).value = '0'; +} +window.resetOffset = resetOffset; + +/** + * Triggered when the music search bar is changed + * @param {MouseEvent} event + */ +export function musiclist_filter(_event) { + const musiclist_element = document.getElementById('client_musiclist'); + const searchname = (document.getElementById('client_musicsearch')).value; + + musiclist_element.innerHTML = ''; + + for (const trackname of client.musics) { + if (trackname.toLowerCase().indexOf(searchname.toLowerCase()) !== -1) { + const newentry = document.createElement('OPTION'); + newentry.text = trackname; + musiclist_element.options.add(newentry); + } + } +} +window.musiclist_filter = musiclist_filter; + +/** + * Triggered when an item on the music list is clicked. + * @param {MouseEvent} event + */ +export function musiclist_click(_event) { + const playtrack = (document.getElementById('client_musiclist')).value; + client.sendMusicChange(playtrack); + + // This is here so you can't actually select multiple tracks, + // even though the select tag has the multiple option to render differently + const musiclist_elements = (document.getElementById('client_musiclist')).selectedOptions; + for (let i = 0; i < musiclist_elements.length; i++) { + musiclist_elements[i].selected = false; + } +} +window.musiclist_click = musiclist_click; + +/** + * Triggered when a character in the mute list is clicked + * @param {MouseEvent} event + */ +export function mutelist_click(_event) { + const mutelist = 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; + +/** + * Triggered when the showname checkboc is clicked + * @param {MouseEvent} event + */ +export function showname_click(_event) { + setCookie('showname', String((document.getElementById('showname')).checked)); + setCookie('ic_chat_name', (document.getElementById('ic_chat_name')).value); + + const css_s = document.getElementById('nameplate_setting'); + + if ((document.getElementById('showname')).checked) { css_s.href = 'styles/shownames.css'; } else { css_s.href = 'styles/nameplates.css'; } +} +window.showname_click = showname_click; + +/** + * Triggered when an item on the area list is clicked. + * @param {MouseEvent} event + */ +export function area_click(el) { + const area = client.areas[el.id.substr(4)].name; + client.sendMusicChange(area); + + const areaHr = document.createElement('div'); + areaHr.className = 'hrtext'; + areaHr.textContent = `switched to ${el.textContent}`; + document.getElementById('client_log').appendChild(areaHr); +} +window.area_click = area_click; + +/** + * Triggered by the music volume slider. + */ +export function changeMusicVolume() { + viewport.musicVolume = (document.getElementById('client_mvolume')).value; + setCookie('musicVolume', viewport.musicVolume); +} +window.changeMusicVolume = changeMusicVolume; + +/** + * Triggered by the blip volume slider. + */ +export function changeBlipVolume() { + const blipVolume = (document.getElementById('client_bvolume')).value; + viewport.blipChannels.forEach((channel) => channel.volume = blipVolume); + setCookie('blipVolume', blipVolume); +} +window.changeBlipVolume = changeBlipVolume; + +/** + * Triggered by the theme selector. + */ +export function reloadTheme() { + viewport.theme = (document.getElementById('client_themeselect')).value; + setCookie('theme', viewport.theme); + (document.getElementById('client_theme')).href = `styles/${viewport.theme}.css`; +} +window.reloadTheme = reloadTheme; + +/** + * Triggered by a changed callword list + */ +export function changeCallwords() { + client.callwords = (document.getElementById('client_callwords')).value.split('\n'); + setCookie('callwords', client.callwords); +} +window.changeCallwords = changeCallwords; + +/** + * Triggered by the modcall sfx dropdown + */ +export function modcall_test() { + client.handleZZ('test#test'.split('#')); +} +window.modcall_test = modcall_test; + +/** + * Triggered by the ini button. + */ +export async function iniedit() { + const ininame = (document.getElementById('client_ininame')).value; + const inicharID = client.charID; + await client.handleCharacterInfo(ininame.split('&'), inicharID); + client.handlePV((`PV#0#CID#${inicharID}`).split('#')); +} +window.iniedit = iniedit; + +/** + * Triggered by the pantilt checkbox + */ +export async function switchPanTilt(addcheck) { + const background = document.getElementById('client_fullview'); + if (addcheck === 1) { + (document.getElementById('client_pantilt')).checked = true; + document.getElementById('client_court').style.display = ''; + } else if (addcheck === 2) { + (document.getElementById('client_pantilt')).checked = false; + document.getElementById('client_court').style.display = 'none'; + } + if ((document.getElementById('client_pantilt')).checked) { + background.style.transition = '0.5s ease-in-out'; + } else { + background.style.transition = 'none'; + } +} +window.switchPanTilt = switchPanTilt; + +/** + * Triggered by the change aspect ratio checkbox + */ +export async function switchAspectRatio() { + const background = document.getElementById('client_background'); + const offsetCheck = document.getElementById('client_hdviewport_offset'); + if ((document.getElementById('client_hdviewport')).checked) { + background.style.paddingBottom = '56.25%'; + offsetCheck.disabled = false; + } else { + background.style.paddingBottom = '75%'; + offsetCheck.disabled = true; + } +} +window.switchAspectRatio = switchAspectRatio; + +/** + * Triggered by the change aspect ratio checkbox + */ +export async function switchChatOffset() { + const container = document.getElementById('client_chatcontainer'); + if ((document.getElementById('client_hdviewport_offset')).checked) { + container.style.width = '80%'; + container.style.left = '10%'; + } else { + container.style.width = '100%'; + container.style.left = '0'; + } +} +window.switchChatOffset = switchChatOffset; + +/** + * Triggered when a character icon is clicked in the character selection menu. + * @param {MouseEvent} event + */ +export function changeCharacter(_event) { + document.getElementById('client_charselect').style.display = 'block'; + document.getElementById('client_emo').innerHTML = ''; +} +window.changeCharacter = changeCharacter; + +/** + * Triggered when there was an error loading a character sprite. + * @param {HTMLImageElement} image the element containing the missing image + */ +export function charError(image) { + console.warn(`${image.src} is missing from webAO`); + image.src = transparentPng; + return true; +} +window.charError = charError; + +/** + * Triggered when there was an error loading a generic sprite. + * @param {HTMLImageElement} image the element containing the missing image + */ +export function imgError(image) { + image.onerror = ''; + image.src = ''; // unload so the old sprite doesn't persist + return true; +} +window.imgError = imgError; + +/** + * Triggered when there was an error loading a sound + * @param {HTMLAudioElement} image the element containing the missing sound + */ +export function opusCheck(channel) { + const audio = channel.src + if (audio === '') { + return + } + console.info(`failed to load sound ${channel.src}`); + let oldsrc = ''; + let newsrc = ''; + oldsrc = channel.src; + if (!oldsrc.endsWith('.opus')) { + newsrc = oldsrc.replace('.mp3', '.opus'); + newsrc = newsrc.replace('.wav', '.opus'); + channel.src = newsrc; // unload so the old sprite doesn't persist + } +} +window.opusCheck = opusCheck; + +/** + * Triggered when the reconnect button is pushed. + */ +export function ReconnectButton() { + client.cleanup(); + client = new Client(serverIP); + if (client) { + mode = 'join'; // HACK: see client.onOpen + + document.getElementById('client_error').style.display = 'none'; + } +} +window.ReconnectButton = ReconnectButton; + +/** + * 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 + */ +function appendICLog(msg, 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) { + client.callwords.forEach(testCallword); + + function testCallword(item) { + if (item !== '' && message.toLowerCase().includes(item.toLowerCase())) { + viewport.sfxaudio.pause(); + viewport.sfxaudio.src = `${AO_HOST}sounds/general/sfx-gallery.opus`; + viewport.sfxaudio.play(); + } + } +} + +/** + * Triggered when the music search bar is changed + * @param {MouseEvent} event + */ +export function chartable_filter(_event) { + const searchname = (document.getElementById('client_charactersearch')).value; + + client.chars.forEach((character, charid) => { + const demothing = document.getElementById(`demo_${charid}`); + if (character.name.toLowerCase().indexOf(searchname.toLowerCase()) === -1) { + demothing.style.display = 'none'; + } else { + demothing.style.display = 'inline-block'; + } + }); +} +window.chartable_filter = chartable_filter; + +/** + * Requests to play as a character. + * @param {number} ccharacter the character ID; if this is a large number, + * then spectator is chosen instead. + */ +export function pickChar(ccharacter) { + if (ccharacter === -1) { + // Spectator + document.getElementById('client_charselect').style.display = 'none'; + } else { + client.sendCharacter(ccharacter); + } +} +window.pickChar = pickChar; + +/** + * Highlights and selects an emotion for in-character chat. + * @param {string} emo the new emotion to be selected + */ +export function pickEmotion(emo) { + try { + if (client.selectedEmote !== -1) { + document.getElementById(`emo_${client.selectedEmote}`).className = 'emote_button'; + } + } catch (err) { + // do nothing + } + client.selectedEmote = emo; + document.getElementById(`emo_${emo}`).className = 'emote_button dark'; + + (document.getElementById('sendsfx')).checked = (client.emote.sfx.length > 1); + + (document.getElementById('sendpreanim')).checked = (client.emote.zoom == 1); +} +window.pickEmotion = pickEmotion; + +/** + * Highlights and selects an evidence for in-character chat. + * @param {string} evidence the evidence to be presented + */ +export function pickEvidence(evidence: number) { + if (client.selectedEvidence !== evidence) { + // Update selected evidence + if (client.selectedEvidence > 0) { + document.getElementById(`evi_${client.selectedEvidence}`).className = 'evi_icon'; + } + document.getElementById(`evi_${evidence}`).className = 'evi_icon dark'; + client.selectedEvidence = evidence; + + // Show evidence on information window + (document.getElementById('evi_name')).value = client.evidences[evidence - 1].name; + (document.getElementById('evi_desc')).value = client.evidences[evidence - 1].desc; + + // Update icon + const icon_id = getIndexFromSelect('evi_select', client.evidences[evidence - 1].filename); + (document.getElementById('evi_select')).selectedIndex = icon_id; + if (icon_id === 0) { + (document.getElementById('evi_filename')).value = client.evidences[evidence - 1].filename; + } + updateEvidenceIcon(); + + // Update button + document.getElementById('evi_add').className = 'client_button hover_button inactive'; + document.getElementById('evi_edit').className = 'client_button hover_button'; + document.getElementById('evi_cancel').className = 'client_button hover_button'; + document.getElementById('evi_del').className = 'client_button hover_button'; + } else { + cancelEvidence(); + } +} +window.pickEvidence = pickEvidence; + +/** + * Add evidence. + */ +export function addEvidence() { + const evidence_select = document.getElementById('evi_select'); + client.sendPE( + (document.getElementById('evi_name')).value, + (document.getElementById('evi_desc')).value, + evidence_select.selectedIndex === 0 + ? (document.getElementById('evi_filename')).value + : evidence_select.options[evidence_select.selectedIndex].text, + ); + cancelEvidence(); +} +window.addEvidence = addEvidence; + +/** + * Edit selected evidence. + */ +export function editEvidence() { + const evidence_select = document.getElementById('evi_select'); + const id = parseInt(client.selectedEvidence) - 1; + client.sendEE( + id, + (document.getElementById('evi_name')).value, + (document.getElementById('evi_desc')).value, + evidence_select.selectedIndex === 0 + ? (document.getElementById('evi_filename')).value + : evidence_select.options[evidence_select.selectedIndex].text, + ); + cancelEvidence(); +} +window.editEvidence = editEvidence; + +/** + * Delete selected evidence. + */ +export function deleteEvidence() { + const id = parseInt(client.selectedEvidence) - 1; + client.sendDE(id); + cancelEvidence(); +} +window.deleteEvidence = deleteEvidence; + +/** + * Cancel evidence selection. + */ +export function cancelEvidence() { + // Clear evidence data + if (client.selectedEvidence > 0) { + document.getElementById(`evi_${client.selectedEvidence}`).className = 'evi_icon'; + } + client.selectedEvidence = 0; + + // Clear evidence on information window + (document.getElementById('evi_select')).selectedIndex = 0; + updateEvidenceIcon(); // Update icon widget + (document.getElementById('evi_filename')).value = ''; + (document.getElementById('evi_name')).value = ''; + (document.getElementById('evi_desc')).value = ''; + (document.getElementById('evi_preview')).src = `${AO_HOST}misc/empty.png`; // Clear icon + + // Update button + document.getElementById('evi_add').className = 'client_button hover_button'; + document.getElementById('evi_edit').className = 'client_button hover_button inactive'; + document.getElementById('evi_cancel').className = 'client_button hover_button inactive'; + document.getElementById('evi_del').className = 'client_button hover_button inactive'; +} +window.cancelEvidence = cancelEvidence; + +/** + * Find index of anything in select box. + * @param {string} select_box the select element name + * @param {string} value the value that need to be compared + */ +export function getIndexFromSelect(select_box: string, value: string) { + // Find if icon alraedy existed in select box + const select_element = document.getElementById(select_box); + for (let i = 1; i < select_element.length; ++i) { + if (select_element.options[i].value === value) { + return i; + } + } + return 0; +} +window.getIndexFromSelect = getIndexFromSelect; + +/** + * Set the style of the chatbox + */ +export function setChatbox(style: string) { + const chatbox_theme = document.getElementById('chatbox_theme'); + const themeselect = document.getElementById('client_chatboxselect'); + const selected_theme = themeselect.value; + + setCookie('chatbox', selected_theme); + if (selected_theme === 'dynamic') { + if (chatbox_arr.includes(style)) { + chatbox_theme.href = `styles/chatbox/${style}.css`; + } else { + chatbox_theme.href = 'styles/chatbox/aa.css'; + } + } else { + chatbox_theme.href = `styles/chatbox/${selected_theme}.css`; + } +} +window.setChatbox = setChatbox; + +/** + * Set the font size for the chatbox + */ +export function resizeChatbox() { + const chatContainerBox = document.getElementById('client_chatcontainer'); + const gameHeight = document.getElementById('client_background').offsetHeight; + + chatContainerBox.style.fontSize = `${(gameHeight * 0.0521).toFixed(1)}px`; +} +window.resizeChatbox = resizeChatbox; + +/** + * Update evidence icon. + */ +export function updateEvidenceIcon() { + const evidence_select = document.getElementById('evi_select'); + const evidence_filename = document.getElementById('evi_filename'); + const evidence_iconbox = document.getElementById('evi_preview'); + + if (evidence_select.selectedIndex === 0) { + evidence_filename.style.display = 'initial'; + evidence_iconbox.src = `${AO_HOST}evidence/${encodeURI(evidence_filename.value.toLowerCase())}`; + } else { + evidence_filename.style.display = 'none'; + evidence_iconbox.src = `${AO_HOST}evidence/${encodeURI(evidence_select.value.toLowerCase())}`; + } +} +window.updateEvidenceIcon = updateEvidenceIcon; + +/** + * Update evidence icon. + */ +export function updateActionCommands(side: string) { + if (side === 'jud') { + document.getElementById('judge_action').style.display = 'inline-table'; + document.getElementById('no_action').style.display = 'none'; + } else { + document.getElementById('judge_action').style.display = 'none'; + document.getElementById('no_action').style.display = 'inline-table'; + } + + // Update role selector + for (let i = 0, role_select = document.getElementById('role_select'); i < role_select.options.length; i++) { + if (side === role_select.options[i].value) { + role_select.options.selectedIndex = i; + return; + } + } +} +window.updateActionCommands = updateActionCommands; + +/** + * Change background via OOC. + */ +export function changeBackgroundOOC() { + const selectedBG = document.getElementById('bg_select'); + const changeBGCommand = "bg $1"; + const bgFilename = document.getElementById('bg_filename'); + + let filename = ''; + if (selectedBG.selectedIndex === 0) { + filename = bgFilename.value; + } else { + filename = selectedBG.value; + } + + if (mode === 'join') { client.sendOOC(`/${changeBGCommand.replace('$1', filename)}`); } else if (mode === 'replay') { client.sendSelf(`BN#${filename}#%`); } +} +window.changeBackgroundOOC = changeBackgroundOOC; + +/** + * Change role via OOC. + */ +export function changeRoleOOC() { + const roleselect = document.getElementById('role_select'); + + client.sendOOC(`/pos ${roleselect.value}`); + client.sendServer(`SP#${roleselect.value}#%`); + updateActionCommands(roleselect.value); +} +window.changeRoleOOC = changeRoleOOC; + +/** + * Random character via OOC. + */ +export function randomCharacterOOC() { + client.sendOOC(`/randomchar`); +} +window.randomCharacterOOC = randomCharacterOOC; + +/** + * Call mod. + */ +export function callMod() { + let modcall; + if (extrafeatures.includes('modcall_reason')) { + modcall = prompt('Please enter the reason for the modcall', ''); + } + if (modcall == null || modcall === '') { + // cancel + } else { + client.sendZZ(modcall); + } +} +window.callMod = callMod; + +/** + * Declare witness testimony. + */ +export function initWT() { + client.sendRT('testimony1'); +} +window.initWT = initWT; + +/** + * Declare cross examination. + */ +export function initCE() { + client.sendRT('testimony2'); +} +window.initCE = initCE; + +/** + * Declare the defendant not guilty + */ +export function notguilty() { + client.sendRT('judgeruling#0'); +} +window.notguilty = notguilty; + +/** + * Declare the defendant not guilty + */ +export function guilty() { + client.sendRT('judgeruling#1'); +} +window.guilty = guilty; + +/** + * Increment defense health point. + */ +export function addHPD() { + client.sendHP(1, String(parseInt(client.hp[0]) + 1)); +} +window.addHPD = addHPD; + +/** + * Decrement defense health point. + */ +export function redHPD() { + client.sendHP(1, String(parseInt(client.hp[0]) - 1)); +} +window.redHPD = redHPD; + +/** + * Increment prosecution health point. + */ +export function addHPP() { + client.sendHP(2, String(parseInt(client.hp[1]) + 1)); +} +window.addHPP = addHPP; + +/** + * Decrement prosecution health point. + */ +export function redHPP() { + client.sendHP(2, String(parseInt(client.hp[1]) - 1)); +} +window.redHPP = redHPP; + +/** + * Update background preview. + */ +export function updateBackgroundPreview() { + const background_select = document.getElementById('bg_select'); + const background_filename = document.getElementById('bg_filename'); + const background_preview = document.getElementById('bg_preview'); + + if (background_select.selectedIndex === 0) { + background_filename.style.display = 'initial'; + background_preview.src = `${AO_HOST}background/${encodeURI(background_filename.value.toLowerCase())}/defenseempty.png`; + } else { + background_filename.style.display = 'none'; + background_preview.src = `${AO_HOST}background/${encodeURI(background_select.value.toLowerCase())}/defenseempty.png`; + } +} +window.updateBackgroundPreview = updateBackgroundPreview; + +/** + * Highlights and selects a menu. + * @param {number} menu the menu to be selected + */ +export function toggleMenu(menu: number) { + if (menu !== selectedMenu) { + document.getElementById(`menu_${menu}`).className = 'menu_button active'; + document.getElementById(`content_${menu}`).className = 'menu_content active'; + document.getElementById(`menu_${selectedMenu}`).className = 'menu_button'; + document.getElementById(`content_${selectedMenu}`).className = 'menu_content'; + selectedMenu = menu; + } +} +window.toggleMenu = toggleMenu; + +/** + * Highlights and selects a shout for in-character chat. + * If the same shout button is selected, then the shout is canceled. + * @param {number} shout the new shout to be selected + */ +export function toggleShout(shout: number) { + if (shout === selectedShout) { + document.getElementById(`button_${shout}`).className = 'client_button'; + selectedShout = 0; + } else { + document.getElementById(`button_${shout}`).className = 'client_button dark'; + if (selectedShout) { + document.getElementById(`button_${selectedShout}`).className = 'client_button'; + } + selectedShout = shout; + } +} +window.toggleShout = toggleShout; +export default Client diff --git a/webAO/dom/changeVolume.js b/webAO/dom/changeVolume.js index 4ef3027..0f7d47e 100644 --- a/webAO/dom/changeVolume.js +++ b/webAO/dom/changeVolume.js @@ -9,6 +9,14 @@ export function changeSFXVolume() { } window.changeSFXVolume = changeSFXVolume; +/** + * Triggered by the testimony volume slider. + */ + export function changeTestimonyVolume() { + setCookie('testimonyVolume', document.getElementById('client_testimonyaudio').volume); +} +window.changeTestimonyVolume = changeTestimonyVolume; + /** * Triggered by the shout volume slider. */ diff --git a/webpack.config.js b/webpack.config.js index 07bca15..c12a11b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -14,7 +14,7 @@ dotenv.config(); module.exports = { entry: { ui: './webAO/ui.js', - client: './webAO/client.js', + client: './webAO/client.ts', master: './webAO/master.ts', dom: glob.sync('./webAO/dom/*.js'), components: glob.sync('./webAO/components/*.js'), -- cgit From 80c134e0f0f018225dcad4d35c6eba0d530ba595 Mon Sep 17 00:00:00 2001 From: stonedDiscord Date: Sat, 26 Mar 2022 16:45:11 +0100 Subject: fix OOC name --- webAO/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webAO/client.ts b/webAO/client.ts index 04a1814..78bd6bf 100644 --- a/webAO/client.ts +++ b/webAO/client.ts @@ -507,7 +507,7 @@ class Client extends EventEmitter { }); // Read cookies and set the UI to its values - (document.getElementById('OOC_name')).value = getCookie('OOC_name') || `web${String(Math.random() * 100 + 10)}`; + (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'; -- cgit From 99a9c76438bfb61d86965f8b260d75b15d1f580b Mon Sep 17 00:00:00 2001 From: stonedDiscord Date: Sat, 26 Mar 2022 17:12:14 +0100 Subject: set types for most things --- webAO/client.ts | 198 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 99 insertions(+), 99 deletions(-) diff --git a/webAO/client.ts b/webAO/client.ts index 78bd6bf..7d1d1de 100644 --- a/webAO/client.ts +++ b/webAO/client.ts @@ -32,8 +32,8 @@ import transparentPng from './constants/transparentPng'; import downloadFile from './services/downloadFile' const version = process.env.npm_package_version; -let client; -let viewport; +let client: Client; +let viewport: Viewport; // Get the arguments from the URL bar let { ip: serverIP, mode, asset, theme, @@ -58,9 +58,9 @@ let oldLoading = false; let selectedMenu = 1; let selectedShout = 0; -let extrafeatures = []; +let extrafeatures: string[] = []; -let hdid; +let hdid: string; declare global { interface Window { @@ -280,7 +280,7 @@ class Client extends EventEmitter { * Hook for sending messages to the server * @param {string} message the message to send */ - sendServer(message) { + sendServer(message: string) { mode === 'replay' ? this.sendSelf(message) : this.serv.send(message); } @@ -288,7 +288,7 @@ class Client extends EventEmitter { * Hook for sending messages to the client * @param {string} message the message to send */ - handleSelf(message) { + handleSelf(message: string) { const message_event = new MessageEvent('websocket', { data: message }); setTimeout(() => this.onMessage(message_event), 1); } @@ -297,7 +297,7 @@ class Client extends EventEmitter { * Hook for sending messages to the client * @param {string} message the message to send */ - sendSelf(message) { + sendSelf(message: string) { (document.getElementById('client_ooclog')).value += `${message}\r\n`; this.handleSelf(message); } @@ -306,7 +306,7 @@ class Client extends EventEmitter { * Sends an out-of-character chat message. * @param {string} message the message to send */ - sendOOC(message) { + sendOOC(message: string) { setCookie('OOC_name', (document.getElementById('OOC_name')).value); const oocName = `${escapeChat(encodeChat((document.getElementById('OOC_name')).value))}`; const oocMessage = `${escapeChat(encodeChat(message))}`; @@ -349,32 +349,32 @@ class Client extends EventEmitter { * @param {number} noninterrupting_preanim play the full preanim (optional) */ sendIC( - deskmod, - preanim, - name, - emote, - message, - side, - sfx_name, - emote_modifier, - sfx_delay, - objection_modifier, - evidence, - flip, - realization, - text_color, - showname, - other_charid, - self_hoffset, - self_yoffset, - noninterrupting_preanim, - looping_sfx, - screenshake, - frame_screenshake, - frame_realization, - frame_sfx, - additive, - effect, + deskmod: string, + preanim: string, + name: string, + emote: string, + message: string, + side: string, + sfx_name: string, + emote_modifier: string, + sfx_delay: string, + objection_modifier: string, + evidence: string, + flip: string, + realization: string, + text_color: string, + showname: string, + other_charid: string, + self_hoffset: string, + self_yoffset: string, + noninterrupting_preanim: string, + looping_sfx: string, + screenshake: string, + frame_screenshake: string, + frame_realization: string, + frame_sfx: string, + additive: string, + effect: string, ) { let extra_cccc = ''; let other_emote = ''; @@ -414,7 +414,7 @@ class Client extends EventEmitter { * @param {string} evidence description * @param {string} evidence image filename */ - sendPE(name, desc, img) { + sendPE(name: string, desc: string, img: string) { this.sendServer(`PE#${escapeChat(encodeChat(name))}#${escapeChat(encodeChat(desc))}#${img}#%`); } @@ -425,7 +425,7 @@ class Client extends EventEmitter { * @param {string} evidence description * @param {string} evidence image filename */ - sendEE(id, name, desc, img) { + sendEE(id: string, name: string, desc: string, img: string) { this.sendServer(`EE#${id}#${escapeChat(encodeChat(name))}#${escapeChat(encodeChat(desc))}#${img}#%`); } @@ -442,7 +442,7 @@ class Client extends EventEmitter { * @param {number} side the position * @param {number} hp the health point */ - sendHP(side, hp) { + sendHP(side: string, hp: string) { this.sendServer(`HP#${side}#${hp}#%`); } @@ -450,7 +450,7 @@ class Client extends EventEmitter { * Sends call mod command. * @param {string} message to mod */ - sendZZ(msg) { + sendZZ(msg: string) { if (extrafeatures.includes('modcall_reason')) { this.sendServer(`ZZ#${msg}#%`); } else { @@ -462,7 +462,7 @@ class Client extends EventEmitter { * Sends testimony command. * @param {string} testimony type */ - sendRT(testimony) { + sendRT(testimony: string) { if (this.chars[this.charID].side === 'jud') { this.sendServer(`RT#${testimony}#%`); } @@ -472,7 +472,7 @@ class Client extends EventEmitter { * Requests to change the music to the specified track. * @param {string} track the track ID */ - sendMusicChange(track) { + sendMusicChange(track: string) { this.sendServer(`MC#${track}#${this.charID}#%`); } @@ -542,7 +542,7 @@ class Client extends EventEmitter { * Requests to play as a specified character. * @param {number} character the character ID */ - sendCharacter(character) { + sendCharacter(character: string) { if (this.chars[character].name) { this.sendServer(`CC#${this.playerID}#${character}#web#%`); } } @@ -550,7 +550,7 @@ class Client extends EventEmitter { * Requests to select a music track. * @param {number?} song the song to be played */ - sendMusic(song) { + sendMusic(song: string) { this.sendServer(`MC#${song}#${this.charID}#%`); } @@ -564,7 +564,7 @@ class Client extends EventEmitter { /** * Triggered when a connection is established to the server. */ - onOpen(_e) { + onOpen(_e: Event) { client.joinServer(); } @@ -572,14 +572,14 @@ class Client extends EventEmitter { * Triggered when the connection to the server closes. * @param {CloseEvent} e */ - onClose(e) { + onClose(e: CloseEvent) { console.error(`The connection was closed: ${e.reason} (${e.code})`); if (extrafeatures.length == 0 && this.banned === false) { document.getElementById('client_errortext').textContent = 'Could not connect to the server'; } document.getElementById('client_error').style.display = 'flex'; document.getElementById('client_loading').style.display = 'none'; - document.getElementById('error_id').textContent = e.code; + document.getElementById('error_id').textContent = String(e.code); this.cleanup(); } @@ -587,7 +587,7 @@ class Client extends EventEmitter { * Triggered when a packet is received from the server. * @param {MessageEvent} e */ - onMessage(e) { + onMessage(e: MessageEvent) { const msg = e.data; console.debug(`S: ${msg}`); @@ -609,10 +609,9 @@ class Client extends EventEmitter { * Triggered when an network error occurs. * @param {ErrorEvent} e */ - onError(e) { - console.error(`A network error occurred: ${e.reason} (${e.code})`); + onError(e: ErrorEvent) { + console.error(`A network error occurred`); document.getElementById('client_error').style.display = 'flex'; - document.getElementById('error_id').textContent = e.code; this.cleanup(); } @@ -680,7 +679,7 @@ class Client extends EventEmitter { * Handles an in-character chat message. * @param {*} args packet arguments */ - handleMS(args) { + handleMS(args: string[]) { // TODO: this if-statement might be a bug. if (args[4] !== viewport.chatmsg.content) { @@ -826,7 +825,7 @@ class Client extends EventEmitter { * Handles an out-of-character chat message. * @param {Array} args packet arguments */ - handleCT(args) { + handleCT(args: string[]) { if (mode !== 'replay') { const oocLog = document.getElementById('client_ooclog'); oocLog.innerHTML += `${prepChat(args[1])}: ${prepChat(args[2])}\r\n`; @@ -841,7 +840,7 @@ class Client extends EventEmitter { * Handles a music change to an arbitrary resource. * @param {Array} args packet arguments */ - handleMC(args) { + handleMC(args: string[]) { const track = prepChat(args[1]); let charID = Number(args[2]); const showname = args[3] || ''; @@ -880,7 +879,7 @@ class Client extends EventEmitter { * Handles a music change to an arbitrary resource, with an offset in seconds. * @param {Array} args packet arguments */ - handleRMC(args) { + handleRMC(args: string[]) { viewport.music.pause(); const { music } = viewport; // Music offset + drift from song loading @@ -897,7 +896,7 @@ class Client extends EventEmitter { * @param {Array} chargs packet arguments * @param {Number} charid character ID */ - async handleCharacterInfo(chargs, charid) { + async handleCharacterInfo(chargs: string[], charid: number) { if (chargs[0]) { let cini: any = {}; const img = document.getElementById(`demo_${charid}`); @@ -984,7 +983,7 @@ class Client extends EventEmitter { * CI#0#Phoenix&description&&&&#Miles ... * @param {Array} args packet arguments */ - handleCI(args) { + handleCI(args: string[]) { // Loop through the 10 characters that were sent for (let i = 2; i <= args.length - 2; i++) { @@ -1004,7 +1003,7 @@ class Client extends EventEmitter { * in one packet. * @param {Array} args packet arguments */ - async handleSC(args) { + async handleSC(args: string[]) { const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); // Add this so people can see characters loading on the screen. @@ -1030,7 +1029,7 @@ class Client extends EventEmitter { * Mostly unimplemented in webAO. * @param {Array} args packet arguments */ - handleEI(args) { + handleEI(args: string[]) { document.getElementById('client_loadingtext').innerHTML = `Loading Evidence ${args[1]}/${this.evidence_list_length}`; this.sendServer('RM#%'); } @@ -1041,7 +1040,7 @@ class Client extends EventEmitter { * * @param {Array} args packet arguments */ - handleLE(args) { + handleLE(args: string[]) { this.evidences = []; for (let i = 1; i < args.length - 1; i++) { const arg = args[i].split('&'); @@ -1087,7 +1086,7 @@ class Client extends EventEmitter { bg_select.innerHTML = ''; bg_select.add(new Option('Custom', '0')); - bg_array.forEach((background) => { + bg_array.forEach((background: string) => { bg_select.add(new Option(background)); }); } catch (err) { @@ -1104,7 +1103,7 @@ class Client extends EventEmitter { const char_select = document.getElementById('client_ininame'); char_select.innerHTML = ''; - char_array.forEach((character) => { + char_array.forEach((character: string) => { char_select.add(new Option(character)); }); } catch (err) { @@ -1121,7 +1120,7 @@ class Client extends EventEmitter { const evi_select = document.getElementById('evi_select'); evi_select.innerHTML = ''; - evi_array.forEach((evi) => { + evi_array.forEach((evi: string) => { evi_select.add(new Option(evi)); }); evi_select.add(new Option('Custom', '0')); @@ -1130,19 +1129,19 @@ class Client extends EventEmitter { } } - isAudio(trackname) { + isAudio(trackname: string) { const audioEndings = ['.wav', '.mp3', '.ogg', '.opus']; return audioEndings.filter((ending) => trackname.endsWith(ending)).length === 1; } - addTrack(trackname) { + addTrack(trackname: string) { const newentry = document.createElement('OPTION'); newentry.text = trackname; (document.getElementById('client_musiclist')).options.add(newentry); this.musics.push(trackname); } - createArea(id, name) { + createArea(id: number, name: string) { const thisarea = { name, players: 0, @@ -1186,7 +1185,7 @@ class Client extends EventEmitter { * per packet. * @param {Array} args packet arguments */ - handleEM(args) { + handleEM(args: string[]) { document.getElementById('client_loadingtext').innerHTML = 'Loading Music'; if (args[1] === '0') { this.resetMusicList(); @@ -1198,7 +1197,7 @@ class Client extends EventEmitter { if (i % 2 === 0) { document.getElementById('client_loadingtext').innerHTML = `Loading Music ${args[1]}/${this.music_list_length}`; const trackname = safeTags(args[i]); - const trackindex = args[i - 1]; + const trackindex = Number(args[i - 1]); if (this.musics_time) { this.addTrack(trackname); } else if (this.isAudio(trackname)) { @@ -1212,14 +1211,14 @@ class Client extends EventEmitter { } // get the next batch of tracks - this.sendServer(`AM#${(args[1] / 10) + 1}#%`); + this.sendServer(`AM#${(Number(args[1]) / 10) + 1}#%`); } /** * Handles incoming music information, containing all music in one packet. * @param {Array} args packet arguments */ - handleSM(args) { + handleSM(args: string[]) { document.getElementById('client_loadingtext').innerHTML = 'Loading Music '; this.resetMusicList(); this.resetAreaList(); @@ -1250,7 +1249,7 @@ class Client extends EventEmitter { * Handles updated music list * @param {Array} args packet arguments */ - handleFM(args) { + handleFM(args: string[]) { this.resetMusicList(); for (let i = 1; i < args.length - 1; i++) { @@ -1263,7 +1262,7 @@ class Client extends EventEmitter { * Handles updated area list * @param {Array} args packet arguments */ - handleFA(args) { + handleFA(args: string[]) { this.resetAreaList(); for (let i = 1; i < args.length - 1; i++) { @@ -1275,7 +1274,7 @@ class Client extends EventEmitter { * Handles the "MusicMode" packet * @param {Array} args packet arguments */ - handleMM(_args) { + handleMM(_args: string[]) { // It's unused nowadays, as preventing people from changing the music is now serverside } @@ -1284,7 +1283,7 @@ class Client extends EventEmitter { * @param {string} type is it a kick or a ban * @param {string} reason why */ - handleBans(type, reason) { + handleBans(type: string, reason: string) { document.getElementById('client_error').style.display = 'flex'; document.getElementById('client_errortext').innerHTML = `${type}:
${reason.replace(/\n/g, '
')}`; (document.getElementsByClassName('client_reconnect')[0]).style.display = 'none'; @@ -1295,7 +1294,7 @@ class Client extends EventEmitter { * Handles the kicked packet * @param {Array} args kick reason */ - handleKK(args) { + handleKK(args: string[]) { this.handleBans('Kicked', safeTags(args[1])); } @@ -1304,7 +1303,7 @@ class Client extends EventEmitter { * this one is sent when you are kicked off the server * @param {Array} args ban reason */ - handleKB(args) { + handleKB(args: string[]) { this.handleBans('Banned', safeTags(args[1])); this.banned = true; } @@ -1314,7 +1313,7 @@ class Client extends EventEmitter { * on client this spawns a message box you can't close for 2 seconds * @param {Array} args ban reason */ - handleBB(args) { + handleBB(args: string[]) { alert(safeTags(args[1])); } @@ -1323,7 +1322,7 @@ class Client extends EventEmitter { * this one is sent when you try to reconnect but you're banned * @param {Array} args ban reason */ - handleBD(args) { + handleBD(args: string[]) { this.handleBans('Banned', safeTags(args[1])); this.banned = true; } @@ -1334,7 +1333,7 @@ class Client extends EventEmitter { * * @param {Array} args packet arguments */ - handleDONE(_args) { + handleDONE(_args: string[]) { document.getElementById('client_loading').style.display = 'none'; if (mode === 'watch') { // Spectators don't need to pick a character document.getElementById('client_charselect').style.display = 'none'; @@ -1346,7 +1345,7 @@ class Client extends EventEmitter { * @param {Array} args packet arguments */ - handleBN(args) { + handleBN(args: string[]) { viewport.bgname = safeTags(args[1]); const bgfolder = viewport.bgFolder; const bg_index = getIndexFromSelect('bg_select', viewport.bgname); @@ -1378,7 +1377,7 @@ class Client extends EventEmitter { * Handles a change in the health bars' states. * @param {Array} args packet arguments */ - handleHP(args) { + handleHP(args: string[]) { const percent_hp = Number(args[2]) * 10; let healthbox; if (args[1] === '1') { @@ -1397,7 +1396,7 @@ class Client extends EventEmitter { * Handles a testimony states. * @param {Array} args packet arguments */ - handleRT(args) { + handleRT(args: string[]) { const judgeid = Number(args[2]); switch (args[1]) { case 'testimony1': @@ -1420,7 +1419,7 @@ class Client extends EventEmitter { * Handles a timer update * @param {Array} args packet arguments */ - handleTI(args) { + handleTI(args: string[]) { const timerid = Number(args[1]); const type = Number(args[2]); const timer_value = args[3]; @@ -1440,7 +1439,7 @@ class Client extends EventEmitter { * Handles a modcall * @param {Array} args packet arguments */ - handleZZ(args) { + handleZZ(args: string[]) { const oocLog = document.getElementById('client_ooclog'); oocLog.innerHTML += `$Alert: ${prepChat(args[1])}\r\n`; if (oocLog.scrollTop > oocLog.scrollHeight - 60) { @@ -1458,7 +1457,7 @@ class Client extends EventEmitter { * Handle the player * @param {Array} args packet arguments */ - handleHI(args) { + handleHI(_args: string[]) { this.sendSelf(`ID#1#webAO#${version}#%`); this.sendSelf('FL#fastloading#yellowtext#cccc_ic_support#flipping#looping_sfx#effects#%'); } @@ -1467,7 +1466,7 @@ class Client extends EventEmitter { * Identifies the server and issues a playerID * @param {Array} args packet arguments */ - handleID(args) { + handleID(args: string[]) { this.playerID = Number(args[1]); const serverSoftware = args[2].split('&')[0]; let serverVersion; @@ -1487,7 +1486,7 @@ class Client extends EventEmitter { * Indicates how many users are on this server * @param {Array} args packet arguments */ - handlePN(_args) { + handlePN(_args: string[]) { this.sendServer('askchaa#%'); } @@ -1495,7 +1494,7 @@ class Client extends EventEmitter { * What? you want a character?? * @param {Array} args packet arguments */ - handleCC(args) { + handleCC(args: string[]) { this.sendSelf(`PV#1#CID#${args[2]}#%`); } @@ -1503,7 +1502,7 @@ class Client extends EventEmitter { * What? you want a character list from me?? * @param {Array} args packet arguments */ - handleaskchaa(_args) { + handleaskchaa(_args: string[]) { this.sendSelf(`SI#${vanilla_character_arr.length}#0#0#%`); } @@ -1511,7 +1510,7 @@ class Client extends EventEmitter { * Handle the change of players in an area. * @param {Array} args packet arguments */ - handleARUP(args) { + handleARUP(args: string[]) { args = args.slice(1); for (let i = 0; i < args.length - 2; i++) { if (this.areas[i]) { // the server sends us ARUP before we even get the area list @@ -1547,7 +1546,7 @@ class Client extends EventEmitter { * With this the server tells us which features it supports * @param {Array} args list of features */ - handleFL(args) { + handleFL(args: string[]) { console.info('Server-supported features:'); console.info(args); extrafeatures = args; @@ -1589,7 +1588,7 @@ class Client extends EventEmitter { * but we use it as a cue to begin retrieving characters. * @param {Array} args packet arguments */ - handleSI(args) { + handleSI(args: string[]) { this.char_list_length = Number(args[1]); this.char_list_length += 1; // some servers count starting from 0 some from 1... this.evidence_list_length = Number(args[2]); @@ -1622,7 +1621,7 @@ class Client extends EventEmitter { * Handles the list of all used and vacant characters. * @param {Array} args list of all characters represented as a 0 for free or a -1 for taken */ - handleCharsCheck(args) { + handleCharsCheck(args: string[]) { for (let i = 0; i < this.char_list_length; i++) { const img = document.getElementById(`demo_${i}`); @@ -1635,7 +1634,7 @@ class Client extends EventEmitter { * PV # playerID (unused) # CID # character ID * @param {Array} args packet arguments */ - async handlePV(args) { + async handlePV(args: string[]) { this.charID = Number(args[3]); document.getElementById('client_charselect').style.display = 'none'; @@ -1721,7 +1720,7 @@ class Client extends EventEmitter { * new asset url!! * @param {Array} args packet arguments */ - handleASS(args) { + handleASS(args: string[]) { AO_HOST = args[1]; } @@ -1729,7 +1728,7 @@ class Client extends EventEmitter { * we are asking ourselves what characters there are * @param {Array} args packet arguments */ - handleRC(_args) { + handleRC(_args: string[]) { this.sendSelf(`SC#${vanilla_character_arr.join('#')}#%`); } @@ -1737,7 +1736,7 @@ class Client extends EventEmitter { * we are asking ourselves what characters there are * @param {Array} args packet arguments */ - handleRM(_args) { + handleRM(_args: string[]) { this.sendSelf(`SM#${vanilla_music_arr.join('#')}#%`); } @@ -1745,7 +1744,7 @@ class Client extends EventEmitter { * we are asking ourselves what characters there are * @param {Array} args packet arguments */ - handleRD(_args) { + handleRD(_args: string[]) { this.sendSelf('BN#gs4#%'); this.sendSelf('DONE#%'); const ooclog = document.getElementById('client_ooclog'); @@ -1858,7 +1857,7 @@ class Viewport { * Sets the volume of the music. * @param {number} volume */ - set musicVolume(volume) { + set musicVolume(volume: number) { this.music.forEach((channel) => channel.volume = volume); } @@ -1874,7 +1873,7 @@ class Viewport { * * @param {string} sfxname */ - async playSFX(sfxname, looping) { + async playSFX(sfxname: string, looping: boolean) { this.sfxaudio.pause(); this.sfxaudio.loop = looping; this.sfxaudio.src = sfxname; @@ -1887,7 +1886,7 @@ class Viewport { * Valid positions: `def, pro, hld, hlp, wit, jud, jur, sea` * @param {string} position the position to change into */ - async changeBackground(position) { + async changeBackground(position: string) { const bgfolder = viewport.bgFolder; const view = document.getElementById('client_fullview'); @@ -2170,6 +2169,7 @@ class Viewport { this.chatmsg.startspeaking = false; } else { this.chatmsg.startspeaking = true; + chatContainerBox.style.opacity = '1'; } this.chatmsg.preanimdelay = gifLength; -- cgit From 64d093af95f4ab8ee5dc15bf2cc51b68c822c45c Mon Sep 17 00:00:00 2001 From: stonedDiscord Date: Sat, 26 Mar 2022 17:38:48 +0100 Subject: type the rest --- webAO/client.ts | 177 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 89 insertions(+), 88 deletions(-) diff --git a/webAO/client.ts b/webAO/client.ts index 7d1d1de..e7336ac 100644 --- a/webAO/client.ts +++ b/webAO/client.ts @@ -160,7 +160,7 @@ class Client extends EventEmitter { checkUpdater: any; _lastTimeICReceived: any; - constructor(address) { + constructor(address: string) { super(); if (mode !== 'replay') { this.serv = new WebSocket(`ws://${address}`); @@ -336,12 +336,12 @@ class Client extends EventEmitter { * @param {string} message the message to be sent * @param {string} side the name of the side in the background * @param {string} sfx_name the name of the sound effect - * @param {string} emote_modifier whether or not to zoom + * @param {number} emote_modifier whether or not to zoom * @param {number} sfx_delay the delay (in milliseconds) to play the sound effect - * @param {string} objection_modifier the number of the shout to play + * @param {number} objection_modifier the number of the shout to play * @param {string} evidence the filename of evidence to show - * @param {number} flip change to 1 to reverse sprite for position changes - * @param {number} realization screen flash effect + * @param {boolean} flip change to 1 to reverse sprite for position changes + * @param {boolean} realization screen flash effect * @param {number} text_color text color * @param {string} showname custom name to be displayed (optional) * @param {number} other_charid paired character (optional) @@ -356,24 +356,24 @@ class Client extends EventEmitter { message: string, side: string, sfx_name: string, - emote_modifier: string, - sfx_delay: string, - objection_modifier: string, - evidence: string, - flip: string, - realization: string, - text_color: string, + emote_modifier: number, + sfx_delay: number, + objection_modifier: number, + evidence: number, + flip: boolean, + realization: boolean, + text_color: number, showname: string, other_charid: string, self_hoffset: string, self_yoffset: string, - noninterrupting_preanim: string, - looping_sfx: string, - screenshake: string, + noninterrupting_preanim: boolean, + looping_sfx: boolean, + screenshake: boolean, frame_screenshake: string, frame_realization: string, frame_sfx: string, - additive: string, + additive: boolean, effect: string, ) { let extra_cccc = ''; @@ -425,7 +425,7 @@ class Client extends EventEmitter { * @param {string} evidence description * @param {string} evidence image filename */ - sendEE(id: string, name: string, desc: string, img: string) { + sendEE(id: number, name: string, desc: string, img: string) { this.sendServer(`EE#${id}#${escapeChat(encodeChat(name))}#${escapeChat(encodeChat(desc))}#${img}#%`); } @@ -433,7 +433,7 @@ class Client extends EventEmitter { * Sends delete evidence command. * @param {number} evidence id */ - sendDE(id) { + sendDE(id: number) { this.sendServer(`DE#${id}#%`); } @@ -442,7 +442,7 @@ class Client extends EventEmitter { * @param {number} side the position * @param {number} hp the health point */ - sendHP(side: string, hp: string) { + sendHP(side: number, hp: number) { this.sendServer(`HP#${side}#${hp}#%`); } @@ -533,7 +533,7 @@ class Client extends EventEmitter { (document.getElementById('ic_chat_name')).value = getCookie('ic_chat_name'); (document.getElementById('showname')).checked = Boolean(getCookie('showname')); - showname_click(0); + showname_click(null); (document.getElementById('client_callwords')).value = getCookie('callwords'); } @@ -542,7 +542,7 @@ class Client extends EventEmitter { * Requests to play as a specified character. * @param {number} character the character ID */ - sendCharacter(character: string) { + sendCharacter(character: number) { if (this.chars[character].name) { this.sendServer(`CC#${this.playerID}#${character}#web#%`); } } @@ -646,7 +646,7 @@ class Client extends EventEmitter { rtime = 0; } - setTimeout(() => onReplayGo(''), rtime); + setTimeout(() => onReplayGo(null), rtime); } } @@ -931,9 +931,9 @@ class Client extends EventEmitter { } const mute_select = document.getElementById('mute_select'); - mute_select.add(new Option(safeTags(chargs[0]), charid)); + mute_select.add(new Option(safeTags(chargs[0]), String(charid))); const pair_select = document.getElementById('pair_select'); - pair_select.add(new Option(safeTags(chargs[0]), charid)); + pair_select.add(new Option(safeTags(chargs[0]), String(charid))); // sometimes ini files lack important settings const default_options = { @@ -990,12 +990,12 @@ class Client extends EventEmitter { if (i % 2 === 0) { document.getElementById('client_loadingtext').innerHTML = `Loading Character ${args[1]}/${this.char_list_length}`; const chargs = args[i].split('&'); - const charid = args[i - 1]; + const charid = Number(args[i - 1]); setTimeout(() => this.handleCharacterInfo(chargs, charid), 500); } } // Request the next pack - this.sendServer(`AN#${(args[1] / 10) + 1}#%`); + this.sendServer(`AN#${(Number(args[1]) / 10) + 1}#%`); } /** @@ -1004,7 +1004,7 @@ class Client extends EventEmitter { * @param {Array} args packet arguments */ async handleSC(args: string[]) { - const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); // Add this so people can see characters loading on the screen. document.getElementById('client_loading').style.display = 'none'; @@ -1382,14 +1382,14 @@ class Client extends EventEmitter { let healthbox; if (args[1] === '1') { // Def hp - this.hp[0] = args[2]; + this.hp[0] = Number(args[2]); healthbox = document.getElementById('client_defense_hp'); } else { // Pro hp - this.hp[1] = args[2]; + this.hp[1] = Number(args[2]); healthbox = document.getElementById('client_prosecutor_hp'); } - healthbox.getElementsByClassName('health-bar')[0].style.width = `${percent_hp}%`; + (healthbox.getElementsByClassName('health-bar')[0]).style.width = `${percent_hp}%`; } /** @@ -1708,7 +1708,7 @@ class Client extends EventEmitter { iniedit_select.add(new Option(safeTags(me.name))); - cswap.forEach((inisw) => iniedit_select.add(new Option(safeTags(inisw)))); + cswap.forEach((inisw: string) => iniedit_select.add(new Option(safeTags(inisw)))); } } catch (err) { console.info("character doesn't have iniswaps"); @@ -1780,6 +1780,7 @@ class Viewport { startFirstTickCheck: boolean; startSecondTickCheck: boolean; startThirdTickCheck: boolean; + theme: string; constructor() { this.textnow = ''; @@ -1818,8 +1819,8 @@ class Viewport { // Allocate multiple blip audio channels to make blips less jittery const blipSelectors = document.getElementsByClassName('blipSound') this.blipChannels = [...blipSelectors]; - this.blipChannels.forEach((channel) => channel.volume = 0.5); - this.blipChannels.forEach((channel) => channel.onerror = opusCheck(channel)); + this.blipChannels.forEach((channel: HTMLAudioElement) => channel.volume = 0.5); + this.blipChannels.forEach((channel: HTMLAudioElement) => channel.onerror = opusCheck(channel)); this.currentBlipChannel = 0; this.sfxaudio = document.getElementById('client_sfxaudio'); @@ -1835,8 +1836,8 @@ class Viewport { const audioChannels = document.getElementsByClassName('audioChannel') this.music = [...audioChannels]; - this.music.forEach((channel) => channel.volume = 0.5); - this.music.forEach((channel) => channel.onerror = opusCheck(channel)); + this.music.forEach((channel: HTMLAudioElement) => channel.volume = 0.5); + this.music.forEach((channel: HTMLAudioElement) => channel.onerror = opusCheck(channel)); this.updater = null; this.testimonyUpdater = null; @@ -1858,7 +1859,7 @@ class Viewport { * @param {number} volume */ set musicVolume(volume: number) { - this.music.forEach((channel) => channel.volume = volume); + this.music.forEach((channel: HTMLAudioElement) => channel.volume = volume); } /** @@ -1891,18 +1892,18 @@ class Viewport { const view = document.getElementById('client_fullview'); - let bench; + let bench: HTMLImageElement; if ('def,pro,wit'.includes(position)) { - bench = document.getElementById(`client_${position}_bench`); + bench = document.getElementById(`client_${position}_bench`); } else { - bench = document.getElementById('client_bench_classic'); + bench = document.getElementById('client_bench_classic'); } - let court; + let court: HTMLImageElement; if ('def,pro,wit'.includes(position)) { - court = document.getElementById(`client_court_${position}`); + court = document.getElementById(`client_court_${position}`); } else { - court = document.getElementById('client_court_classic'); + court = document.getElementById('client_court_classic'); } const positions = { @@ -2072,7 +2073,7 @@ class Viewport { * TODO: the preanim logic, on the other hand, should probably be moved to tick() * @param {object} chatmsg the new chat message */ - async say(chatmsg) { + async say(chatmsg: any) { this.chatmsg = chatmsg; this.textnow = ''; @@ -2204,7 +2205,7 @@ class Viewport { // flip the paired character pairLayers.style.transform = this.chatmsg.other_flip === 1 ? 'scaleX(-1)' : 'scaleX(1)'; - this.blipChannels.forEach((channel) => channel.src = `${AO_HOST}sounds/general/sfx-blip${encodeURI(this.chatmsg.blips.toLowerCase())}.opus`); + this.blipChannels.forEach((channel: HTMLAudioElement) => channel.src = `${AO_HOST}sounds/general/sfx-blip${encodeURI(this.chatmsg.blips.toLowerCase())}.opus`); // process markup if (this.chatmsg.content.startsWith('~~')) { @@ -2233,7 +2234,7 @@ class Viewport { this.tick(); } - handleTextTick(charLayers) { + handleTextTick(charLayers: HTMLImageElement) { const chatBox = document.getElementById('client_chat'); const waitingBox = document.getElementById('client_chatwaiting'); const chatBoxInner = document.getElementById('client_inner_chat'); @@ -2303,12 +2304,12 @@ class Viewport { const shoutSprite = document.getElementById('client_shout'); const effectlayer = document.getElementById('client_fg'); const chatBoxInner = document.getElementById('client_inner_chat'); - let charLayers = document.getElementById('client_char'); - let pairLayers = document.getElementById('client_pair_char'); + let charLayers = document.getElementById('client_char'); + let pairLayers = document.getElementById('client_pair_char'); if ('def,pro,wit'.includes(this.chatmsg.side)) { - charLayers = document.getElementById(`client_${this.chatmsg.side}_char`); - pairLayers = document.getElementById(`client_${this.chatmsg.side}_pair_char`); + charLayers = document.getElementById(`client_${this.chatmsg.side}_char`); + pairLayers = document.getElementById(`client_${this.chatmsg.side}_pair_char`); } const charName = this.chatmsg.name.toLowerCase(); @@ -2429,7 +2430,7 @@ class Viewport { * Triggered when the Return key is pressed on the out-of-character chat input box. * @param {KeyboardEvent} event */ -export function onOOCEnter(event) { +export function onOOCEnter(event: KeyboardEvent) { if (event.keyCode === 13) { client.sendOOC((document.getElementById('client_oocinputbox')).value); (document.getElementById('client_oocinputbox')).value = ''; @@ -2441,7 +2442,7 @@ window.onOOCEnter = onOOCEnter; * Triggered when the user click replay GOOOOO * @param {KeyboardEvent} event */ -export function onReplayGo(_event) { +export function onReplayGo(_event: Event) { client.handleReplay(); } window.onReplayGo = onReplayGo; @@ -2450,24 +2451,24 @@ window.onReplayGo = onReplayGo; * Triggered when the Return key is pressed on the in-character chat input box. * @param {KeyboardEvent} event */ -export function onEnter(event) { +export function onEnter(event: KeyboardEvent) { if (event.keyCode === 13) { const mychar = client.character; const myemo = client.emote; const evi = client.evidence; - const flip = ((document.getElementById('button_flip').classList.contains('dark')) ? 1 : 0); - const flash = ((document.getElementById('button_flash').classList.contains('dark')) ? 1 : 0); - const screenshake = ((document.getElementById('button_shake').classList.contains('dark')) ? 1 : 0); - const noninterrupting_preanim = (((document.getElementById('check_nonint')).checked) ? 1 : 0); - const looping_sfx = (((document.getElementById('check_loopsfx')).checked) ? 1 : 0); - const color = (document.getElementById('textcolor')).value; + const flip = Boolean((document.getElementById('button_flip').classList.contains('dark'))); + const flash = Boolean((document.getElementById('button_flash').classList.contains('dark'))); + const screenshake = Boolean((document.getElementById('button_shake').classList.contains('dark'))); + const noninterrupting_preanim = Boolean(((document.getElementById('check_nonint')).checked)); + const looping_sfx = Boolean(((document.getElementById('check_loopsfx')).checked)); + const color = Number((document.getElementById('textcolor')).value); const showname = (document.getElementById('ic_chat_name')).value; const text = (document.getElementById('client_inputbox')).value; const pairchar = (document.getElementById('pair_select')).value; const pairoffset = (document.getElementById('pair_offset')).value; const pairyoffset = (document.getElementById('pair_y_offset')).value; const myrole = (document.getElementById('role_select')).value ? (document.getElementById('role_select')).value : mychar.side; - const additive = (((document.getElementById('check_additive')).checked) ? 1 : 0); + const additive = Boolean(((document.getElementById('check_additive')).checked)); const effect = (document.getElementById('effect_select')).value; let sfxname = '0'; @@ -2534,7 +2535,7 @@ function resetICParams() { } } -export function resetOffset(_event) { +export function resetOffset(_event: Event) { (document.getElementById('pair_offset')).value = '0'; (document.getElementById('pair_y_offset')).value = '0'; } @@ -2544,7 +2545,7 @@ window.resetOffset = resetOffset; * Triggered when the music search bar is changed * @param {MouseEvent} event */ -export function musiclist_filter(_event) { +export function musiclist_filter(_event: Event) { const musiclist_element = document.getElementById('client_musiclist'); const searchname = (document.getElementById('client_musicsearch')).value; @@ -2564,7 +2565,7 @@ window.musiclist_filter = musiclist_filter; * Triggered when an item on the music list is clicked. * @param {MouseEvent} event */ -export function musiclist_click(_event) { +export function musiclist_click(_event: Event) { const playtrack = (document.getElementById('client_musiclist')).value; client.sendMusicChange(playtrack); @@ -2581,7 +2582,7 @@ window.musiclist_click = musiclist_click; * Triggered when a character in the mute list is clicked * @param {MouseEvent} event */ -export function mutelist_click(_event) { +export function mutelist_click(_event: Event) { const mutelist = document.getElementById('mute_select'); const selected_character = mutelist.options[mutelist.selectedIndex]; @@ -2600,7 +2601,7 @@ window.mutelist_click = mutelist_click; * Triggered when the showname checkboc is clicked * @param {MouseEvent} event */ -export function showname_click(_event) { +export function showname_click(_event: Event) { setCookie('showname', String((document.getElementById('showname')).checked)); setCookie('ic_chat_name', (document.getElementById('ic_chat_name')).value); @@ -2612,9 +2613,9 @@ window.showname_click = showname_click; /** * Triggered when an item on the area list is clicked. - * @param {MouseEvent} event + * @param {HTMLElement} el */ -export function area_click(el) { +export function area_click(el: HTMLElement) { const area = client.areas[el.id.substr(4)].name; client.sendMusicChange(area); @@ -2629,8 +2630,8 @@ window.area_click = area_click; * Triggered by the music volume slider. */ export function changeMusicVolume() { - viewport.musicVolume = (document.getElementById('client_mvolume')).value; - setCookie('musicVolume', viewport.musicVolume); + viewport.musicVolume = Number((document.getElementById('client_mvolume')).value); + setCookie('musicVolume', String(viewport.musicVolume)); } window.changeMusicVolume = changeMusicVolume; @@ -2639,7 +2640,7 @@ window.changeMusicVolume = changeMusicVolume; */ export function changeBlipVolume() { const blipVolume = (document.getElementById('client_bvolume')).value; - viewport.blipChannels.forEach((channel) => channel.volume = blipVolume); + viewport.blipChannels.forEach((channel: HTMLAudioElement) => channel.volume = Number(blipVolume)); setCookie('blipVolume', blipVolume); } window.changeBlipVolume = changeBlipVolume; @@ -2685,7 +2686,7 @@ window.iniedit = iniedit; /** * Triggered by the pantilt checkbox */ -export async function switchPanTilt(addcheck) { +export async function switchPanTilt(addcheck: number) { const background = document.getElementById('client_fullview'); if (addcheck === 1) { (document.getElementById('client_pantilt')).checked = true; @@ -2737,7 +2738,7 @@ window.switchChatOffset = switchChatOffset; * Triggered when a character icon is clicked in the character selection menu. * @param {MouseEvent} event */ -export function changeCharacter(_event) { +export function changeCharacter(_event: Event) { document.getElementById('client_charselect').style.display = 'block'; document.getElementById('client_emo').innerHTML = ''; } @@ -2747,7 +2748,7 @@ window.changeCharacter = changeCharacter; * Triggered when there was an error loading a character sprite. * @param {HTMLImageElement} image the element containing the missing image */ -export function charError(image) { +export function charError(image: HTMLImageElement) { console.warn(`${image.src} is missing from webAO`); image.src = transparentPng; return true; @@ -2758,8 +2759,8 @@ window.charError = charError; * Triggered when there was an error loading a generic sprite. * @param {HTMLImageElement} image the element containing the missing image */ -export function imgError(image) { - image.onerror = ''; +export function imgError(image: HTMLImageElement) { + image.onerror = null; image.src = ''; // unload so the old sprite doesn't persist return true; } @@ -2769,7 +2770,7 @@ window.imgError = imgError; * Triggered when there was an error loading a sound * @param {HTMLAudioElement} image the element containing the missing sound */ -export function opusCheck(channel) { +export function opusCheck(channel: HTMLAudioElement) { const audio = channel.src if (audio === '') { return @@ -2805,7 +2806,7 @@ window.ReconnectButton = ReconnectButton; * @param {string} msg the string to be added * @param {string} name the name of the sender */ -function appendICLog(msg, showname = '', nameplate = '', time = new Date()) { +function appendICLog(msg: string, showname = '', nameplate = '', time = new Date()) { const entry = document.createElement('p'); const shownameField = document.createElement('span'); const nameplateField = document.createElement('span'); @@ -2849,10 +2850,10 @@ function appendICLog(msg, showname = '', nameplate = '', time = new Date()) { * check if the message contains an entry on our callword list * @param {string} message */ -export function checkCallword(message) { +export function checkCallword(message: string) { client.callwords.forEach(testCallword); - function testCallword(item) { + function testCallword(item: string) { if (item !== '' && message.toLowerCase().includes(item.toLowerCase())) { viewport.sfxaudio.pause(); viewport.sfxaudio.src = `${AO_HOST}sounds/general/sfx-gallery.opus`; @@ -2865,10 +2866,10 @@ export function checkCallword(message) { * Triggered when the music search bar is changed * @param {MouseEvent} event */ -export function chartable_filter(_event) { +export function chartable_filter(_event: Event) { const searchname = (document.getElementById('client_charactersearch')).value; - client.chars.forEach((character, charid) => { + client.chars.forEach((character: any, charid: number) => { const demothing = document.getElementById(`demo_${charid}`); if (character.name.toLowerCase().indexOf(searchname.toLowerCase()) === -1) { demothing.style.display = 'none'; @@ -2884,7 +2885,7 @@ window.chartable_filter = chartable_filter; * @param {number} ccharacter the character ID; if this is a large number, * then spectator is chosen instead. */ -export function pickChar(ccharacter) { +export function pickChar(ccharacter: number) { if (ccharacter === -1) { // Spectator document.getElementById('client_charselect').style.display = 'none'; @@ -2898,7 +2899,7 @@ window.pickChar = pickChar; * Highlights and selects an emotion for in-character chat. * @param {string} emo the new emotion to be selected */ -export function pickEmotion(emo) { +export function pickEmotion(emo: number) { try { if (client.selectedEmote !== -1) { document.getElementById(`emo_${client.selectedEmote}`).className = 'emote_button'; @@ -2972,7 +2973,7 @@ window.addEvidence = addEvidence; */ export function editEvidence() { const evidence_select = document.getElementById('evi_select'); - const id = parseInt(client.selectedEvidence) - 1; + const id = client.selectedEvidence - 1; client.sendEE( id, (document.getElementById('evi_name')).value, @@ -2989,7 +2990,7 @@ window.editEvidence = editEvidence; * Delete selected evidence. */ export function deleteEvidence() { - const id = parseInt(client.selectedEvidence) - 1; + const id = client.selectedEvidence - 1; client.sendDE(id); cancelEvidence(); } @@ -3201,7 +3202,7 @@ window.guilty = guilty; * Increment defense health point. */ export function addHPD() { - client.sendHP(1, String(parseInt(client.hp[0]) + 1)); + client.sendHP(1, (client.hp[0] + 1)); } window.addHPD = addHPD; @@ -3209,7 +3210,7 @@ window.addHPD = addHPD; * Decrement defense health point. */ export function redHPD() { - client.sendHP(1, String(parseInt(client.hp[0]) - 1)); + client.sendHP(1, (client.hp[0] - 1)); } window.redHPD = redHPD; @@ -3217,7 +3218,7 @@ window.redHPD = redHPD; * Increment prosecution health point. */ export function addHPP() { - client.sendHP(2, String(parseInt(client.hp[1]) + 1)); + client.sendHP(2, (client.hp[1] + 1)); } window.addHPP = addHPP; @@ -3225,7 +3226,7 @@ window.addHPP = addHPP; * Decrement prosecution health point. */ export function redHPP() { - client.sendHP(2, String(parseInt(client.hp[1]) - 1)); + client.sendHP(2, (client.hp[1] - 1)); } window.redHPP = redHPP; -- cgit From 6c11facbfc4ee14a5baf7530d304eef59ce1d25c Mon Sep 17 00:00:00 2001 From: stonedDiscord Date: Sat, 26 Mar 2022 17:53:14 +0100 Subject: fix bool effects --- webAO/client.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/webAO/client.ts b/webAO/client.ts index e7336ac..52ae68b 100644 --- a/webAO/client.ts +++ b/webAO/client.ts @@ -93,7 +93,7 @@ declare global { pickChar: (ccharacter: any) => void; chartable_filter: (_event: any) => void; ReconnectButton: (_event: any) => void; - opusCheck: (channel: any) => void; + opusCheck: (channel: HTMLAudioElement) => OnErrorEventHandlerNonNull; imgError: (image: any) => void; charError: (image: any) => void; changeCharacter: (_event: any) => void; @@ -391,16 +391,16 @@ class Client extends EventEmitter { extra_cccc = `${showname}#${other_charid}${other_emote}#${self_offset}${other_offset}#${noninterrupting_preanim}#`; if (extrafeatures.includes('looping_sfx')) { - extra_27 = `${looping_sfx}#${screenshake}#${frame_screenshake}#${frame_realization}#${frame_sfx}#`; + extra_27 = `${Number(looping_sfx)}#${screenshake}#${frame_screenshake}#${frame_realization}#${frame_sfx}#`; if (extrafeatures.includes('effects')) { - extra_28 = `${additive}#${effect}#`; + extra_28 = `${Number(additive)}#${effect}#`; } } } const serverMessage = `MS#${deskmod}#${preanim}#${name}#${emote}` + `#${escapeChat(encodeChat(message))}#${side}#${sfx_name}#${emote_modifier}` - + `#${this.charID}#${sfx_delay}#${objection_modifier}#${evidence}#${flip}#${realization}#${text_color}#${extra_cccc}${extra_27}${extra_28}%`; + + `#${this.charID}#${sfx_delay}#${Number(objection_modifier)}#${Number(evidence)}#${Number(flip)}#${realization}#${text_color}#${extra_cccc}${extra_27}${extra_28}%`; this.sendServer(serverMessage); if (mode === 'replay') { @@ -1162,7 +1162,7 @@ class Client extends EventEmitter { + `CM: ${thisarea.cm}\n` + `Area lock: ${thisarea.locked}`; newarea.onclick = function () { - area_click(this); + area_click(newarea); }; document.getElementById('areas').appendChild(newarea); @@ -2660,7 +2660,7 @@ window.reloadTheme = reloadTheme; */ export function changeCallwords() { client.callwords = (document.getElementById('client_callwords')).value.split('\n'); - setCookie('callwords', client.callwords); + setCookie('callwords', client.callwords.join('\n')); } window.changeCallwords = changeCallwords; @@ -2770,7 +2770,7 @@ window.imgError = imgError; * Triggered when there was an error loading a sound * @param {HTMLAudioElement} image the element containing the missing sound */ -export function opusCheck(channel: HTMLAudioElement) { +export function opusCheck(channel: HTMLAudioElement): OnErrorEventHandlerNonNull{ const audio = channel.src if (audio === '') { return -- cgit From f93b3b49ae3e2d6fbc2bd17a8566b0ce723f71a8 Mon Sep 17 00:00:00 2001 From: stonedDiscord Date: Sat, 26 Mar 2022 17:55:25 +0100 Subject: forgot screenshake --- webAO/client.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webAO/client.ts b/webAO/client.ts index 52ae68b..6559982 100644 --- a/webAO/client.ts +++ b/webAO/client.ts @@ -388,10 +388,10 @@ class Client extends EventEmitter { other_emote = '##'; other_offset = '#0#0'; } - extra_cccc = `${showname}#${other_charid}${other_emote}#${self_offset}${other_offset}#${noninterrupting_preanim}#`; + extra_cccc = `${showname}#${other_charid}${other_emote}#${self_offset}${other_offset}#${Number(noninterrupting_preanim)}#`; if (extrafeatures.includes('looping_sfx')) { - extra_27 = `${Number(looping_sfx)}#${screenshake}#${frame_screenshake}#${frame_realization}#${frame_sfx}#`; + extra_27 = `${Number(looping_sfx)}#${Number(screenshake)}#${frame_screenshake}#${frame_realization}#${frame_sfx}#`; if (extrafeatures.includes('effects')) { extra_28 = `${Number(additive)}#${effect}#`; } @@ -400,7 +400,7 @@ class Client extends EventEmitter { const serverMessage = `MS#${deskmod}#${preanim}#${name}#${emote}` + `#${escapeChat(encodeChat(message))}#${side}#${sfx_name}#${emote_modifier}` - + `#${this.charID}#${sfx_delay}#${Number(objection_modifier)}#${Number(evidence)}#${Number(flip)}#${realization}#${text_color}#${extra_cccc}${extra_27}${extra_28}%`; + + `#${this.charID}#${sfx_delay}#${Number(objection_modifier)}#${Number(evidence)}#${Number(flip)}#${Number(realization)}#${text_color}#${extra_cccc}${extra_27}${extra_28}%`; this.sendServer(serverMessage); if (mode === 'replay') { -- cgit From 6cf4cca93dbe72b11c52b3bb5df1537c1d782999 Mon Sep 17 00:00:00 2001 From: stonedDiscord Date: Sat, 26 Mar 2022 17:58:22 +0100 Subject: type offset --- webAO/client.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/webAO/client.ts b/webAO/client.ts index 6559982..1818531 100644 --- a/webAO/client.ts +++ b/webAO/client.ts @@ -365,8 +365,8 @@ class Client extends EventEmitter { text_color: number, showname: string, other_charid: string, - self_hoffset: string, - self_yoffset: string, + self_hoffset: number, + self_yoffset: number, noninterrupting_preanim: boolean, looping_sfx: boolean, screenshake: boolean, @@ -2465,8 +2465,8 @@ export function onEnter(event: KeyboardEvent) { const showname = (document.getElementById('ic_chat_name')).value; const text = (document.getElementById('client_inputbox')).value; const pairchar = (document.getElementById('pair_select')).value; - const pairoffset = (document.getElementById('pair_offset')).value; - const pairyoffset = (document.getElementById('pair_y_offset')).value; + const pairoffset = Number((document.getElementById('pair_offset')).value); + const pairyoffset = Number((document.getElementById('pair_y_offset')).value); const myrole = (document.getElementById('role_select')).value ? (document.getElementById('role_select')).value : mychar.side; const additive = Boolean(((document.getElementById('check_additive')).checked)); const effect = (document.getElementById('effect_select')).value; -- cgit From bda49615f436ec662d29eae91ef3265fcaa05e2f Mon Sep 17 00:00:00 2001 From: Caleb Mabry Date: Sat, 26 Mar 2022 13:13:39 -0400 Subject: Allow strict --- tsconfig.json | 2 +- webAO/client.ts | 50 ++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 16f950d..9422b00 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "allowJs": true, "target": "es5", "lib": ["DOM","DOM.Iterable"], - "strict": false, + "strict": true, "strictNullChecks": false, //document.getElementBy "downlevelIteration": true }, diff --git a/webAO/client.ts b/webAO/client.ts index 1818531..54bc760 100644 --- a/webAO/client.ts +++ b/webAO/client.ts @@ -34,10 +34,21 @@ const version = process.env.npm_package_version; let client: Client; let viewport: Viewport; +interface Testimony { + [key: number]: string +} + // Get the arguments from the URL bar +interface QueryParams { + ip: string + serverIP: string + mode: string + asset: string + theme: string +} let { ip: serverIP, mode, asset, theme, -} = queryParser(); +} = queryParser() as QueryParams; // Unless there is an asset URL specified, use the wasabi one const DEFAULT_HOST = 'http://attorneyoffline.de/base/'; let AO_HOST = asset || DEFAULT_HOST; @@ -1906,45 +1917,59 @@ class Viewport { court = document.getElementById('client_court_classic'); } - const positions = { + 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' }, + desk: { ao2: 'defensedesk.png', ao1: 'bancodefensa.png' } as Desk, speedLines: 'defense_speedlines.gif', }, pro: { bg: 'prosecutorempty', - desk: { ao2: 'prosecutiondesk.png', ao1: 'bancoacusacion.png' }, + desk: { ao2: 'prosecutiondesk.png', ao1: 'bancoacusacion.png' } as Desk, speedLines: 'prosecution_speedlines.gif', }, hld: { bg: 'helperstand', - desk: null, + desk: null as Desk, speedLines: 'defense_speedlines.gif', }, hlp: { bg: 'prohelperstand', - desk: null, + desk: null as Desk, speedLines: 'prosecution_speedlines.gif', }, wit: { bg: 'witnessempty', - desk: { ao2: 'stand.png', ao1: 'estrado.png' }, + desk: { ao2: 'stand.png', ao1: 'estrado.png' } as Desk, speedLines: 'prosecution_speedlines.gif', }, jud: { bg: 'judgestand', - desk: { ao2: 'judgedesk.png', ao1: 'judgedesk.gif' }, + desk: { ao2: 'judgedesk.png', ao1: 'judgedesk.gif' } as Desk, speedLines: 'prosecution_speedlines.gif', }, jur: { bg: 'jurystand', - desk: { ao2: 'jurydesk.png', ao1: 'estrado.png' }, + desk: { ao2: 'jurydesk.png', ao1: 'estrado.png' } as Desk, speedLines: 'defense_speedlines.gif', }, sea: { bg: 'seancestand', - desk: { ao2: 'seancedesk.png', ao1: 'estrado.png' }, + desk: { ao2: 'seancedesk.png', ao1: 'estrado.png' } as Desk, speedLines: 'prosecution_speedlines.gif', }, }; @@ -2004,7 +2029,8 @@ class Viewport { * Intialize testimony updater */ initTestimonyUpdater() { - const testimonyFilenames = { + + const testimonyFilenames: Testimony = { 1: 'witnesstestimony', 2: 'crossexamination', 3: 'notguilty', @@ -2032,7 +2058,7 @@ class Viewport { * Updates the testimony overaly */ updateTestimony() { - const testimonyFilenames = { + const testimonyFilenames: Testimony = { 1: 'witnesstestimony', 2: 'crossexamination', 3: 'notguilty', -- cgit From b277d7ea90be4975edae4daf55365a2b5a720188 Mon Sep 17 00:00:00 2001 From: Caleb Mabry Date: Sat, 26 Mar 2022 13:46:49 -0400 Subject: Ticks then tick again! --- webAO/client.ts | 50 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/webAO/client.ts b/webAO/client.ts index 54bc760..9b60359 100644 --- a/webAO/client.ts +++ b/webAO/client.ts @@ -145,6 +145,7 @@ fpPromise isLowMemory(); client.loadResources(); }); +const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); let lastICMessageTime = new Date(0); @@ -2260,7 +2261,7 @@ class Viewport { this.tick(); } - handleTextTick(charLayers: HTMLImageElement) { + async handleTextTick(charLayers: HTMLImageElement) { const chatBox = document.getElementById('client_chat'); const waitingBox = document.getElementById('client_chatwaiting'); const chatBoxInner = document.getElementById('client_inner_chat'); @@ -2276,7 +2277,36 @@ class Viewport { this.textnow = this.chatmsg.content.substring(0, this.textnow.length + 1); const characterElement = this.chatmsg.parsed[this.textnow.length - 1] if (characterElement) { - chatBoxInner.appendChild(this.chatmsg.parsed[this.textnow.length - 1]); + const COMMAND_IDENTIFIER = '\\' + + const nextCharacterElement = this.chatmsg.parsed[this.textnow.length] + const flash = async () => { + const effectlayer = document.getElementById('client_fg'); + this.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'); + this.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 + })) + + if (characterElement.innerHTML === COMMAND_IDENTIFIER && commands.has(nextCharacterElement?.innerHTML)) { + this.textnow = this.chatmsg.content.substring(0, this.textnow.length + 1); + await commands.get(nextCharacterElement.innerHTML)() + } else { + chatBoxInner.appendChild(this.chatmsg.parsed[this.textnow.length - 1]); + } } // scroll to bottom @@ -2319,9 +2349,11 @@ class Viewport { * * XXX: This relies on a global variable `this.chatmsg`! */ - tick() { - if (this._animating) { - this.updater = setTimeout(() => this.tick(), UPDATE_INTERVAL); + async tick() { + await delay(UPDATE_INTERVAL) + + if (this.textnow === this.chatmsg.content) { + return } const gamewindow = document.getElementById('client_gamewindow'); @@ -2385,7 +2417,7 @@ class Viewport { if (this.textnow !== this.chatmsg.content && hasNonInterruptingPreAnim) { const chatContainerBox = document.getElementById('client_chatcontainer'); chatContainerBox.style.opacity = '1'; - this.handleTextTick(charLayers) + await this.handleTextTick(charLayers) }else if (isShoutAndPreanimOver && this.startSecondTickCheck) { if (this.chatmsg.startspeaking) { @@ -2436,9 +2468,10 @@ class Viewport { waitingBox.style.opacity = '1'; this._animating = false; clearTimeout(this.updater); + return } } else if (this.textnow !== this.chatmsg.content) { - this.handleTextTick(charLayers) + await this.handleTextTick(charLayers) } } @@ -2448,6 +2481,9 @@ class Viewport { this.playSFX(`${AO_HOST}sounds/general/${encodeURI(this.chatmsg.sound.toLowerCase())}.opus`, this.chatmsg.looping_sfx); } } + if (this._animating) { + this.tick() + } this.tickTimer += UPDATE_INTERVAL; } } -- cgit From c32eea5367295c4ed653ea519ab1963459880f0c Mon Sep 17 00:00:00 2001 From: Caleb Mabry Date: Sat, 26 Mar 2022 16:32:32 -0400 Subject: Text speed --- webAO/client.ts | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/webAO/client.ts b/webAO/client.ts index 9b60359..aa000f7 100644 --- a/webAO/client.ts +++ b/webAO/client.ts @@ -744,6 +744,7 @@ class Client extends EventEmitter { flip: Number(args[13]), flash: Number(args[14]), color: Number(args[15]), + speed: UPDATE_INTERVAL }; if (extrafeatures.includes('cccc_ic_support')) { @@ -1806,6 +1807,7 @@ class Viewport { color: 0, snddelay: 0, preanimdelay: 0, + speed: UPDATE_INTERVAL }; this.shouts = [ @@ -2260,7 +2262,7 @@ class Viewport { this.chatmsg.parsed = await attorneyMarkdown.applyMarkdown(chatmsg.content, this.colors[this.chatmsg.color]) this.tick(); } - + async handleTextTick(charLayers: HTMLImageElement) { const chatBox = document.getElementById('client_chat'); const waitingBox = document.getElementById('client_chatwaiting'); @@ -2300,7 +2302,28 @@ class Viewport { 's': shake, 'f': flash })) - + const textSpeeds = new Set(['{', '}']) + + // Changing Text Speed + if (textSpeeds.has(characterElement.innerHTML)) { + // Grab them all in a row + const index = this.textnow.length + for(let i = this.textnow.length; i < this.chatmsg.content.length; i++) { + const currentCharacter = this.chatmsg.parsed[i - 1].innerHTML + if (currentCharacter === '{') { + if (this.chatmsg.speed > 0) { + this.chatmsg.speed -= 20 + } + } else if(currentCharacter === '}') { + this.chatmsg.speed += 20 + } else { + // No longer at a speed character + this.textnow = this.chatmsg.content.substring(0, i); + break + } + } + } + if (characterElement.innerHTML === COMMAND_IDENTIFIER && commands.has(nextCharacterElement?.innerHTML)) { this.textnow = this.chatmsg.content.substring(0, this.textnow.length + 1); await commands.get(nextCharacterElement.innerHTML)() @@ -2350,7 +2373,7 @@ class Viewport { * XXX: This relies on a global variable `this.chatmsg`! */ async tick() { - await delay(UPDATE_INTERVAL) + await delay(this.chatmsg.speed) if (this.textnow === this.chatmsg.content) { return -- cgit From 7386f4cbec11b138e49d94a24f5a2368eb009994 Mon Sep 17 00:00:00 2001 From: Caleb Mabry Date: Sat, 26 Mar 2022 17:00:45 -0400 Subject: Adding limits of 120 ms for slowness --- webAO/client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webAO/client.ts b/webAO/client.ts index aa000f7..9c1598d 100644 --- a/webAO/client.ts +++ b/webAO/client.ts @@ -2307,14 +2307,14 @@ class Viewport { // Changing Text Speed if (textSpeeds.has(characterElement.innerHTML)) { // Grab them all in a row - const index = this.textnow.length + const MAX_SLOW_CHATSPEED = 120 for(let i = this.textnow.length; i < this.chatmsg.content.length; i++) { const currentCharacter = this.chatmsg.parsed[i - 1].innerHTML if (currentCharacter === '{') { if (this.chatmsg.speed > 0) { this.chatmsg.speed -= 20 } - } else if(currentCharacter === '}') { + } else if(currentCharacter === '}' && this.chatmsg.speed < MAX_SLOW_CHATSPEED) { this.chatmsg.speed += 20 } else { // No longer at a speed character -- cgit From 24f5b0970e87a879967bdb1d6b15b5a90b8148d8 Mon Sep 17 00:00:00 2001 From: Caleb Mabry Date: Sat, 26 Mar 2022 17:02:14 -0400 Subject: Only do it if match closing character --- webAO/client.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/webAO/client.ts b/webAO/client.ts index 9c1598d..ac34334 100644 --- a/webAO/client.ts +++ b/webAO/client.ts @@ -2314,8 +2314,10 @@ class Viewport { if (this.chatmsg.speed > 0) { this.chatmsg.speed -= 20 } - } else if(currentCharacter === '}' && this.chatmsg.speed < MAX_SLOW_CHATSPEED) { - this.chatmsg.speed += 20 + } else if(currentCharacter === '}') { + if(this.chatmsg.speed < MAX_SLOW_CHATSPEED) { + this.chatmsg.speed += 20 + } } else { // No longer at a speed character this.textnow = this.chatmsg.content.substring(0, i); -- cgit From fa175d7b0136cd837942eb54db0c98c4314eaada Mon Sep 17 00:00:00 2001 From: stonedDiscord Date: Mon, 28 Mar 2022 21:30:35 +0200 Subject: list http links even when coming from a https page --- webAO/master.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/webAO/master.ts b/webAO/master.ts index a48f4e9..ca129fd 100644 --- a/webAO/master.ts +++ b/webAO/master.ts @@ -132,6 +132,7 @@ function cachedServerlist(response: Response) { } function processServerlist(thelist: { name: string, description: string, ip: string, port: number, ws_port: number, assets: string, online: string }[]) { + const myURL: string = window.location.href.replace('https://','http://'); for (let i = 0; i < thelist.length - 1; i++) { const serverEntry: { name: string, description: string, ip: string, port: number, ws_port: number, assets: string, online: string } = thelist[i]; @@ -142,8 +143,8 @@ function processServerlist(thelist: { name: string, description: string, ip: str if (serverEntry.ws_port) { document.getElementById('masterlist').innerHTML += `
  • ${safeTags(serverEntry.name)}

    ` - + `
    Watch` - + `Join
  • `; + + `Watch` + + `Join`; } } } -- cgit From 1d88577978b370ab3c96f007aa1cb646bdf6bf35 Mon Sep 17 00:00:00 2001 From: stonedDiscord Date: Mon, 28 Mar 2022 21:50:09 +0200 Subject: don't need double slashes --- webAO/master.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webAO/master.ts b/webAO/master.ts index ca129fd..5ac63b3 100644 --- a/webAO/master.ts +++ b/webAO/master.ts @@ -143,8 +143,8 @@ function processServerlist(thelist: { name: string, description: string, ip: str if (serverEntry.ws_port) { document.getElementById('masterlist').innerHTML += `
  • ${safeTags(serverEntry.name)}

    ` - + `Watch` - + `Join
  • `; + + `Watch` + + `Join`; } } } -- cgit From d3911aa9ad6bc16c70355fe11d1377d636b14565 Mon Sep 17 00:00:00 2001 From: stonedDiscord Date: Mon, 28 Mar 2022 21:50:31 +0200 Subject: what was this still doing here --- webAO/master.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/webAO/master.ts b/webAO/master.ts index 5ac63b3..5c538d4 100644 --- a/webAO/master.ts +++ b/webAO/master.ts @@ -150,7 +150,6 @@ function processServerlist(thelist: { name: string, description: string, ip: str } function processVersion(data: string) { - console.debug(data); document.getElementById('clientinfo').innerHTML = `Client version: ${version}`; document.getElementById('serverinfo').innerHTML = `Master server version: ${data}`; } -- cgit