aboutsummaryrefslogtreecommitdiff
path: root/webAO
diff options
context:
space:
mode:
Diffstat (limited to 'webAO')
-rw-r--r--webAO/client.ts3389
-rw-r--r--webAO/client/__tests__/setEmote.test.js11
-rw-r--r--webAO/client/addTrack.ts15
-rw-r--r--webAO/client/aoHost.ts (renamed from webAO/client/aoHost.js)6
-rw-r--r--webAO/client/appendICLog.ts57
-rw-r--r--webAO/client/checkCallword.ts17
-rw-r--r--webAO/client/createArea.ts30
-rw-r--r--webAO/client/fetchLists.ts60
-rw-r--r--webAO/client/fixLastArea.ts15
-rw-r--r--webAO/client/handleBans.ts17
-rw-r--r--webAO/client/handleCharacterInfo.ts105
-rw-r--r--webAO/client/isAudio.ts6
-rw-r--r--webAO/client/isLowMemory.ts10
-rw-r--r--webAO/client/loadResources.ts81
-rw-r--r--webAO/client/resetICParams.ts21
-rw-r--r--webAO/client/saveChatLogHandle.ts26
-rw-r--r--webAO/client/sender/index.ts68
-rw-r--r--webAO/client/sender/sendCharacter.ts11
-rw-r--r--webAO/client/sender/sendCheck.ts8
-rw-r--r--webAO/client/sender/sendDE.ts9
-rw-r--r--webAO/client/sender/sendEE.ts16
-rw-r--r--webAO/client/sender/sendHP.ts10
-rw-r--r--webAO/client/sender/sendIC.ts106
-rw-r--r--webAO/client/sender/sendMusic.ts10
-rw-r--r--webAO/client/sender/sendMusicChange.ts10
-rw-r--r--webAO/client/sender/sendOOC.ts33
-rw-r--r--webAO/client/sender/sendPE.ts14
-rw-r--r--webAO/client/sender/sendRT.ts9
-rw-r--r--webAO/client/sender/sendSelf.ts13
-rw-r--r--webAO/client/sender/sendServer.ts10
-rw-r--r--webAO/client/sender/sendZZ.ts13
-rw-r--r--webAO/client/setEmote.js40
-rw-r--r--webAO/client/setEmote.ts55
-rw-r--r--webAO/components/blip.js2
-rw-r--r--webAO/dom/addEvidence.ts20
-rw-r--r--webAO/dom/addHPD.ts9
-rw-r--r--webAO/dom/addHPP.ts9
-rw-r--r--webAO/dom/areaClick.ts15
-rw-r--r--webAO/dom/callMod.ts16
-rw-r--r--webAO/dom/cancelEvidence.ts36
-rw-r--r--webAO/dom/changeBackgroundOOC.ts28
-rw-r--r--webAO/dom/changeBlipVolume.ts15
-rw-r--r--webAO/dom/changeCallwords.ts13
-rw-r--r--webAO/dom/changeCharacter.ts11
-rw-r--r--webAO/dom/changeMusicVolume.ts14
-rw-r--r--webAO/dom/changeRoleOOC.ts13
-rw-r--r--webAO/dom/charError.ts12
-rw-r--r--webAO/dom/charTableFilter.ts20
-rw-r--r--webAO/dom/deleteEvidence.ts12
-rw-r--r--webAO/dom/editEvidence.ts22
-rw-r--r--webAO/dom/getIndexFromSelect.ts16
-rw-r--r--webAO/dom/guilty.ts9
-rw-r--r--webAO/dom/imgError.ts10
-rw-r--r--webAO/dom/iniEdit.ts15
-rw-r--r--webAO/dom/initCE.ts9
-rw-r--r--webAO/dom/initWT.ts9
-rw-r--r--webAO/dom/modCallTest.ts8
-rw-r--r--webAO/dom/musicListClick.ts21
-rw-r--r--webAO/dom/musicListFilter.ts24
-rw-r--r--webAO/dom/notGuilty.ts9
-rw-r--r--webAO/dom/onEnter.ts103
-rw-r--r--webAO/dom/onOOCEnter.ts15
-rw-r--r--webAO/dom/onReplayGo.ts10
-rw-r--r--webAO/dom/opusCheck.ts22
-rw-r--r--webAO/dom/pickChar.ts16
-rw-r--r--webAO/dom/pickEmotion.ts24
-rw-r--r--webAO/dom/pickEvidence.ts51
-rw-r--r--webAO/dom/randomCharacterOOC.ts8
-rw-r--r--webAO/dom/reconnectButton.ts16
-rw-r--r--webAO/dom/redHPD.ts9
-rw-r--r--webAO/dom/redHPP.ts9
-rw-r--r--webAO/dom/reloadTheme.ts16
-rw-r--r--webAO/dom/resetOffset.ts6
-rw-r--r--webAO/dom/resizeChatbox.ts33
-rw-r--r--webAO/dom/setChatbox.ts28
-rw-r--r--webAO/dom/showNameClick.ts26
-rw-r--r--webAO/dom/switchAspectRatio.ts19
-rw-r--r--webAO/dom/switchChatOffset.ts17
-rw-r--r--webAO/dom/switchPanTilt.ts16
-rw-r--r--webAO/dom/toggleElement.js13
-rw-r--r--webAO/dom/toggleMenu.ts18
-rw-r--r--webAO/dom/toggleShout.ts21
-rw-r--r--webAO/dom/updateActionCommands.ts27
-rw-r--r--webAO/dom/updateBackgroundPreview.ts28
-rw-r--r--webAO/dom/updateEvidenceIcon.ts28
-rw-r--r--webAO/dom/window.ts56
-rw-r--r--webAO/encoding.ts8
-rw-r--r--webAO/index.ts0
-rw-r--r--webAO/master.ts13
-rw-r--r--webAO/packets/handlers/handleARUP.ts42
-rw-r--r--webAO/packets/handlers/handleASS.ts10
-rw-r--r--webAO/packets/handlers/handleBB.ts11
-rw-r--r--webAO/packets/handlers/handleBD.ts14
-rw-r--r--webAO/packets/handlers/handleBN.ts84
-rw-r--r--webAO/packets/handlers/handleCC.ts9
-rw-r--r--webAO/packets/handlers/handleCI.ts27
-rw-r--r--webAO/packets/handlers/handleCT.ts17
-rw-r--r--webAO/packets/handlers/handleCharsCheck.ts17
-rw-r--r--webAO/packets/handlers/handleDONE.ts16
-rw-r--r--webAO/packets/handlers/handleEI.ts30
-rw-r--r--webAO/packets/handlers/handleEM.ts43
-rw-r--r--webAO/packets/handlers/handleFA.ts15
-rw-r--r--webAO/packets/handlers/handleFL.ts48
-rw-r--r--webAO/packets/handlers/handleFM.ts16
-rw-r--r--webAO/packets/handlers/handleHI.ts14
-rw-r--r--webAO/packets/handlers/handleHP.ts23
-rw-r--r--webAO/packets/handlers/handleID.ts24
-rw-r--r--webAO/packets/handlers/handleJD.ts13
-rw-r--r--webAO/packets/handlers/handleKB.ts13
-rw-r--r--webAO/packets/handlers/handleKK.ts10
-rw-r--r--webAO/packets/handlers/handleLE.ts35
-rw-r--r--webAO/packets/handlers/handleMC.ts43
-rw-r--r--webAO/packets/handlers/handleMM.ts8
-rw-r--r--webAO/packets/handlers/handleMS.ts165
-rw-r--r--webAO/packets/handlers/handlePN.ts9
-rw-r--r--webAO/packets/handlers/handlePV.ts85
-rw-r--r--webAO/packets/handlers/handleRC.ts10
-rw-r--r--webAO/packets/handlers/handleRD.ts18
-rw-r--r--webAO/packets/handlers/handleRM.ts10
-rw-r--r--webAO/packets/handlers/handleRMC.ts24
-rw-r--r--webAO/packets/handlers/handleRT.ts25
-rw-r--r--webAO/packets/handlers/handleSC.ts38
-rw-r--r--webAO/packets/handlers/handleSI.ts41
-rw-r--r--webAO/packets/handlers/handleSM.ts41
-rw-r--r--webAO/packets/handlers/handleSP.ts8
-rw-r--r--webAO/packets/handlers/handleTI.ts21
-rw-r--r--webAO/packets/handlers/handleZZ.ts23
-rw-r--r--webAO/packets/handlers/handleackMS.ts8
-rw-r--r--webAO/packets/handlers/handleaskchaa.ts10
-rw-r--r--webAO/packets/ms.js28
-rw-r--r--webAO/packets/packetHandler.ts3
-rw-r--r--webAO/packets/packets.ts86
-rw-r--r--webAO/styles/chatbox/aa.css9
-rw-r--r--webAO/styles/chatbox/acww.css142
-rw-r--r--webAO/styles/chatbox/bricks.svg11
-rw-r--r--webAO/styles/chatbox/chat999.css8
-rw-r--r--webAO/styles/chatbox/chatdd.css8
-rw-r--r--webAO/styles/chatbox/chatdr2.css30
-rw-r--r--webAO/styles/chatbox/chatfuture.css8
-rw-r--r--webAO/styles/chatbox/chatp3.css4
-rw-r--r--webAO/styles/chatbox/chatplvsaa.css36
-rw-r--r--webAO/styles/chatbox/chatwaiting_acww.svg51
-rw-r--r--webAO/styles/chatbox/ddlc.css18
-rw-r--r--webAO/styles/chatbox/dgs.css8
-rw-r--r--webAO/styles/chatbox/dr1.css4
-rw-r--r--webAO/styles/chatbox/drae.css8
-rw-r--r--webAO/styles/chatbox/drv3.css24
-rw-r--r--webAO/styles/chatbox/drv3chatbox.pngbin36885 -> 33880 bytes
-rw-r--r--webAO/styles/chatbox/drv3trackstatus.pngbin0 -> 18455 bytes
-rw-r--r--webAO/styles/chatbox/drv3trackstatustext.pngbin0 -> 21648 bytes
-rw-r--r--webAO/styles/chatbox/ff.css8
-rw-r--r--webAO/styles/chatbox/halla.css8
-rw-r--r--webAO/styles/chatbox/homestuck.css8
-rw-r--r--webAO/styles/chatbox/key.css71
-rw-r--r--webAO/styles/chatbox/legacy.css8
-rw-r--r--webAO/styles/chatbox/music_dr2.svg64
-rw-r--r--webAO/styles/chatbox/n64zelda.css8
-rw-r--r--webAO/styles/chatbox/p4.css4
-rw-r--r--webAO/styles/chatbox/p5.css4
-rw-r--r--webAO/styles/chatbox/papermario.css8
-rw-r--r--webAO/styles/chatbox/plvspw.pngbin0 -> 213 bytes
-rw-r--r--webAO/styles/chatbox/plvspw_name.pngbin0 -> 211 bytes
-rw-r--r--webAO/styles/chatbox/trilogy.css8
-rw-r--r--webAO/styles/chatbox/whentheycry.css8
-rw-r--r--webAO/styles/chatbox/yakuza.css8
-rw-r--r--webAO/styles/chatbox/yttd.css8
-rw-r--r--webAO/styles/client.css7
-rw-r--r--webAO/styles/effects/rain.css23
-rw-r--r--webAO/utils/__tests__/paths.test.ts13
-rw-r--r--webAO/utils/calculateApngLength.js13
-rw-r--r--webAO/utils/getCookie.ts (renamed from webAO/utils/getCookie.js)2
-rw-r--r--webAO/utils/getResources.js8
-rw-r--r--webAO/utils/paths.ts1
-rw-r--r--webAO/utils/queryParser.js9
-rw-r--r--webAO/utils/queryParser.ts22
-rw-r--r--webAO/utils/setCookie.ts (renamed from webAO/utils/setCookie.js)4
-rw-r--r--webAO/viewport/constants/colors.ts11
-rw-r--r--webAO/viewport/constants/defaultChatMsg.ts15
-rw-r--r--webAO/viewport/constants/positions.ts45
-rw-r--r--webAO/viewport/constants/shouts.ts1
-rw-r--r--webAO/viewport/interfaces/ChatMsg.ts34
-rw-r--r--webAO/viewport/interfaces/Desk.ts4
-rw-r--r--webAO/viewport/interfaces/Position.ts7
-rw-r--r--webAO/viewport/interfaces/Positions.ts5
-rw-r--r--webAO/viewport/interfaces/Testimony.ts3
-rw-r--r--webAO/viewport/interfaces/Viewport.ts43
-rw-r--r--webAO/viewport/utils/createBlipChannels.ts15
-rw-r--r--webAO/viewport/utils/createMusic.ts13
-rw-r--r--webAO/viewport/utils/createSfxAudio.ts9
-rw-r--r--webAO/viewport/utils/createShoutAudio.ts9
-rw-r--r--webAO/viewport/utils/createTestimonyAudio.ts9
-rw-r--r--webAO/viewport/utils/handleICSpeaking.ts312
-rw-r--r--webAO/viewport/utils/initTestimonyUpdater.ts31
-rw-r--r--webAO/viewport/utils/setSide.ts91
-rw-r--r--webAO/viewport/viewport.ts498
195 files changed, 5045 insertions, 3393 deletions
diff --git a/webAO/client.ts b/webAO/client.ts
index 508a844..6d1483b 100644
--- a/webAO/client.ts
+++ b/webAO/client.ts
@@ -2,154 +2,83 @@
* 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;
+ */
+import { isLowMemory } from './client/isLowMemory'
+import FingerprintJS from "@fingerprintjs/fingerprintjs";
+import { sender, ISender } from './client/sender/index'
+import queryParser from "./utils/queryParser";
+import getResources from "./utils/getResources.js";
+import masterViewport from "./viewport/viewport";
+import { Viewport } from './viewport/interfaces/Viewport';
+import { EventEmitter } from "events";
+import { onReplayGo } from './dom/onReplayGo'
+import { packetHandler } from './packets/packetHandler'
+import { loadResources } from './client/loadResources'
+import { AO_HOST } from './client/aoHost'
+import { fetchBackgroundList, fetchEvidenceList } from './client/fetchLists'
+let { ip: serverIP, mode, theme } = queryParser();
-let client: Client;
-let viewport: Viewport;
-interface Testimony {
- [key: number]: string
+let THEME: string = theme || "default";
+export let CHATBOX: string;
+export const setCHATBOX = (val: string) => {
+ CHATBOX = val
}
-
-// Get the arguments from the URL bar
-interface QueryParams {
- ip: string
- serverIP: string
- mode: string
- asset: string
- theme: string
+export let client: Client;
+export const setClient = (val: Client) => {
+ client = val
}
-let {
- ip: serverIP, mode, asset, theme,
-} = 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;
-const THEME = theme || 'default';
-const attorneyMarkdown = mlConfig(AO_HOST)
-
-const UPDATE_INTERVAL = 60;
+export 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;
+export let oldLoading = false;
+export const setOldLoading = (val: boolean) => {
+ oldLoading = val
+}
// presettings
-let selectedMenu = 1;
-let selectedShout = 0;
-
-let extrafeatures: string[] = [];
-
-let hdid: string;
-
-declare global {
- interface Window {
- handleCredentialResponse: (response: any) => void;
- 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: HTMLAudioElement) => OnErrorEventHandlerNonNull;
- 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;
- }
+export let selectedMenu = 1;
+export const setSelectedMenu = (val: number) => {
+ selectedMenu = val
+}
+export let selectedShout = 0;
+export const setSelectedShout = (val: number) => {
+ selectedShout = val
+}
+export let extrafeatures: string[] = [];
+export const setExtraFeatures = (val: any) => {
+ extrafeatures = val
}
-function isLowMemory() {
- if (/webOS|iPod|BlackBerry|BB|PlayBook|IEMobile|Windows Phone|Kindle|Silk|PlayStation|Nintendo|Opera Mini/i.test(navigator.userAgent)) {
- oldLoading = true;
- }
+export let banned: boolean = false;
+export const setBanned = (val: boolean) => {
+ banned = val
}
+let hdid: string;
+
const fpPromise = FingerprintJS.load();
+
fpPromise
.then((fp) => fp.get())
.then((result) => {
hdid = result.visitorId;
- client = new Client(serverIP);
- viewport = new Viewport();
- isLowMemory();
+ client = new Client(serverIP);
+ client.connect()
+ client.isLowMemory();
client.loadResources();
});
-const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
-let lastICMessageTime = new Date(0);
+export const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
+export let lastICMessageTime = new Date(0);
+export const setLastICMessageTime = (val: Date) => {
+ lastICMessageTime = val
+}
class Client extends EventEmitter {
serv: any;
hp: number[];
@@ -170,493 +99,180 @@ class Client extends EventEmitter {
resources: any;
selectedEmote: number;
selectedEvidence: number;
+ sender: ISender;
checkUpdater: any;
_lastTimeICReceived: any;
-
+ viewport: Viewport;
+ connect: () => void;
+ loadResources: () => void
+ isLowMemory: () => void
constructor(address: string) {
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.connect = () => {
+ 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));
+ 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.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('2F', this.handle2F.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.sender = sender
+ this.viewport = masterViewport();
this._lastTimeICReceived = new Date(0);
+ loadResources
+ isLowMemory
}
/**
- * Gets the current player's character.
- */
+ * Gets the current player's character.
+ */
get character() {
return this.chars[this.charID];
}
/**
- * Gets the player's currently selected emote.
- */
+ * 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
- */
+ * 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: string) {
- mode === 'replay' ? this.sendSelf(message) : this.serv.send(message);
+ return document.getElementById("button_present").classList.contains("dark")
+ ? this.selectedEvidence
+ : 0;
}
/**
- * Hook for sending messages to the client
- * @param {string} message the message to send
- */
+ * Hook for sending messages to the client
+ * @param {string} message the message to send
+ */
handleSelf(message: string) {
- const message_event = new MessageEvent('websocket', { data: 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: string) {
- (<HTMLInputElement>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: string) {
- setCookie('OOC_name', (<HTMLInputElement>document.getElementById('OOC_name')).value);
- const oocName = `${escapeChat(encodeChat((<HTMLInputElement>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 {number} emote_modifier whether or not to zoom
- * @param {number} sfx_delay the delay (in milliseconds) to play the sound effect
- * @param {number} objection_modifier the number of the shout to play
- * @param {string} evidence the filename of evidence to show
- * @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)
- * @param {number} self_offset offset to paired character (optional)
- * @param {number} noninterrupting_preanim play the full preanim (optional)
- */
- sendIC(
- deskmod: string,
- preanim: string,
- name: string,
- emote: string,
- message: string,
- side: string,
- sfx_name: 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: number,
- self_yoffset: number,
- noninterrupting_preanim: boolean,
- looping_sfx: boolean,
- screenshake: boolean,
- frame_screenshake: string,
- frame_realization: string,
- frame_sfx: string,
- additive: boolean,
- effect: string,
- ) {
- 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}<and>${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}#${Number(noninterrupting_preanim)}#`;
-
- if (extrafeatures.includes('looping_sfx')) {
- extra_27 = `${Number(looping_sfx)}#${Number(screenshake)}#${frame_screenshake}#${frame_realization}#${frame_sfx}#`;
- if (extrafeatures.includes('effects')) {
- extra_28 = `${Number(additive)}#${effect}#`;
- }
- }
- }
-
- 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)}#${Number(realization)}#${text_color}#${extra_cccc}${extra_27}${extra_28}%`;
-
- this.sendServer(serverMessage);
- if (mode === 'replay') {
- (<HTMLInputElement>document.getElementById('client_ooclog')).value += `wait#${(<HTMLInputElement>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: string, desc: string, img: string) {
- 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: number, name: string, desc: string, img: string) {
- this.sendServer(`EE#${id}#${escapeChat(encodeChat(name))}#${escapeChat(encodeChat(desc))}#${img}#%`);
- }
-
- /**
- * Sends delete evidence command.
- * @param {number} evidence id
- */
- sendDE(id: number) {
- this.sendServer(`DE#${id}#%`);
- }
-
- /**
- * Sends health point command.
- * @param {number} side the position
- * @param {number} hp the health point
- */
- sendHP(side: number, hp: number) {
- this.sendServer(`HP#${side}#${hp}#%`);
- }
-
- /**
- * Sends call mod command.
- * @param {string} message to mod
- */
- sendZZ(msg: string) {
- if (extrafeatures.includes('modcall_reason')) {
- this.sendServer(`ZZ#${msg}#%`);
- } else {
- this.sendServer('ZZ#%');
- }
- }
-
- /**
- * Sends testimony command.
- * @param {string} testimony type
- */
- sendRT(testimony: string) {
- 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: string) {
- this.sendServer(`MC#${track}#${this.charID}#%`);
- }
-
- /**
- * Begins the handshake process by sending an identifier
- * to the server.
- */
+ * Begins the handshake process by sending an identifier
+ * to the server.
+ */
joinServer() {
- this.sendServer('ID#webAO#webAO#%');
- this.sendServer(`HI#${hdid}#%`);
- 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 = <HTMLSelectElement>document.getElementById('bg_select');
- background_select.add(new Option('Custom', '0'));
- vanilla_background_arr.forEach((background) => {
- background_select.add(new Option(background));
- });
-
- // Load evidence array to select
- const evidence_select = <HTMLSelectElement>document.getElementById('evi_select');
- evidence_select.add(new Option('Custom', '0'));
- vanilla_evidence_arr.forEach((evidence) => {
- evidence_select.add(new Option(evidence));
- });
-
- // Read cookies and set the UI to its values
- (<HTMLInputElement>document.getElementById('OOC_name')).value = getCookie('OOC_name') || `web${String(Math.round(Math.random() * 100 + 10))}`;
-
- // Read cookies and set the UI to its values
- const cookietheme = getCookie('theme') || 'default';
-
- (<HTMLOptionElement>document.querySelector(`#client_themeselect [value="${cookietheme}"]`)).selected = true;
- reloadTheme();
-
- const cookiechatbox = getCookie('chatbox') || 'dynamic';
-
- (<HTMLOptionElement>document.querySelector(`#client_chatboxselect [value="${cookiechatbox}"]`)).selected = true;
- setChatbox(cookiechatbox);
-
- (<HTMLInputElement>document.getElementById('client_mvolume')).value = getCookie('musicVolume') || '1';
- changeMusicVolume();
- (<HTMLAudioElement>document.getElementById('client_sfxaudio')).volume = Number(getCookie('sfxVolume')) || 1;
- changeSFXVolume();
- (<HTMLAudioElement>document.getElementById('client_shoutaudio')).volume = Number(getCookie('shoutVolume')) || 1;
- changeShoutVolume();
- (<HTMLAudioElement>document.getElementById('client_testimonyaudio')).volume = Number(getCookie('testimonyVolume')) || 1;
- changeTestimonyVolume();
- (<HTMLInputElement>document.getElementById('client_bvolume')).value = getCookie('blipVolume') || '1';
- changeBlipVolume();
-
- (<HTMLInputElement>document.getElementById('ic_chat_name')).value = getCookie('ic_chat_name');
- (<HTMLInputElement>document.getElementById('showname')).checked = Boolean(getCookie('showname'));
- showname_click(null);
-
- (<HTMLInputElement>document.getElementById('client_callwords')).value = getCookie('callwords');
- }
-
- /**
- * Requests to play as a specified character.
- * @param {number} character the character ID
- */
- sendCharacter(character: number) {
- 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: string) {
- this.sendServer(`MC#${song}#${this.charID}#%`);
- }
-
- /**
- * Sends a keepalive packet.
- */
- sendCheck() {
- this.sendServer(`CH#${this.charID}#%`);
+ this.sender.sendServer(`HI#${hdid}#%`);
+ this.sender.sendServer("ID#webAO#webAO#%");
+ if (mode !== "replay") {
+ this.checkUpdater = setInterval(() => this.sender.sendCheck(), 5000);
+ }
}
/**
- * Triggered when a connection is established to the server.
- */
+ * Triggered when a connection is established to the server.
+ */
onOpen(_e: Event) {
client.joinServer();
}
/**
- * Triggered when the connection to the server closes.
- * @param {CloseEvent} e
- */
+ * Triggered when the connection to the server closes.
+ * @param {CloseEvent} 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_waiting').style.display = 'block';
- document.getElementById('client_error').style.display = 'flex';
- document.getElementById('client_loading').style.display = 'none';
- document.getElementById('error_id').textContent = String(e.code);
+ if (extrafeatures.length == 0 && banned === false) {
+ document.getElementById("client_errortext").textContent =
+ "Could not connect to the server";
+ }
+ document.getElementById("client_waiting").style.display = "block";
+ document.getElementById("client_error").style.display = "flex";
+ document.getElementById("client_loading").style.display = "none";
+ document.getElementById("error_id").textContent = String(e.code);
this.cleanup();
}
/**
- * Triggered when a packet is received from the server.
- * @param {MessageEvent} e
- */
+ * Triggered when a packet is received from the server.
+ * @param {MessageEvent} e
+ */
onMessage(e: MessageEvent) {
const msg = e.data;
console.debug(`S: ${msg}`);
- const lines = msg.split('%');
-
- for (const msg of lines) {
- if (msg === '') { break; }
+ const data = msg.split("%")[0];
+ const splitPacket = data.split('#')
+ const packetHeader = splitPacket[0];
- const args = msg.split('#');
- const header = args[0];
-
- if (!this.emit(header, args)) {
- console.warn(`Invalid packet header ${header}`);
- }
- }
+ packetHandler.has(packetHeader)
+ ? packetHandler.get(packetHeader)(splitPacket)
+ : console.warn(`Invalid packet header ${packetHeader}`);
}
/**
- * Triggered when an network error occurs.
- * @param {ErrorEvent} e
- */
+ * Triggered when an network error occurs.
+ * @param {ErrorEvent} e
+ */
onError(e: ErrorEvent) {
console.error(`A network error occurred`);
- document.getElementById('client_error').style.display = 'flex';
+ document.getElementById("client_error").style.display = "flex";
this.cleanup();
}
/**
- * Stop sending keepalives to the server.
- */
+ * 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 = '';
+ this.serv.close();
}
/**
- * Parse the lines in the OOC and play them
- * @param {*} args packet arguments
- */
+ * Parse the lines in the OOC and play them
+ * @param {*} args packet arguments
+ */
handleReplay() {
- const ooclog = <HTMLInputElement>document.getElementById('client_ooclog');
+ const ooclog = <HTMLInputElement>document.getElementById("client_ooclog");
const rawLog = false;
- let rtime: number = Number((<HTMLInputElement>document.getElementById('client_replaytimer')).value);
+ let rtime: number = Number(
+ (<HTMLInputElement>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') {
+ 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;
}
@@ -664,2738 +280,19 @@ class Client extends EventEmitter {
}
}
- saveChatlogHandle = async () => {
- const clientLog = document.getElementById('client_log')
- const icMessageLogs = clientLog.getElementsByTagName('p')
- const messages = []
-
- for (let i = 0; i < icMessageLogs.length; i++) {
- const SHOWNAME_POSITION = 0
- const TEXT_POSITION = 2
- const showname = icMessageLogs[i].children[SHOWNAME_POSITION].innerHTML
- const text = icMessageLogs[i].children[TEXT_POSITION].innerHTML
- const message = `${showname}: ${text}`
- messages.push(message)
- }
- const d = new Date();
- let ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(d);
- let mo = new Intl.DateTimeFormat('en', { month: 'short' }).format(d);
- let da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(d);
-
- const filename = `chatlog-${da}-${mo}-${ye}`.toLowerCase();
- downloadFile(messages.join('\n'), filename);
-
- // Reset Chatbox to Empty
- (<HTMLInputElement>document.getElementById('client_inputbox')).value = '';
- }
-
- /**
- * Handles an in-character chat message.
- * @param {*} args packet arguments
- */
- handleMS(args: string[]) {
-
- // 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]),
- speed: UPDATE_INTERVAL
- };
-
- if (extrafeatures.includes('cccc_ic_support')) {
- const extra_cccc = {
- showname: safeTags(args[16]),
- other_charid: Number(args[17]),
- other_name: safeTags(args[18]),
- other_emote: safeTags(args[19]),
- self_offset: args[20].split('<and>'), // HACK: here as well, client is fucked and uses this instead of &
- other_offset: args[21].split('<and>'),
- other_flip: Number(args[22]),
- noninterrupting_preanim: Number(args[23]),
- };
- 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: string[]) {
- 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: string[]) {
- 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: string[]) {
- 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: string[], charid: number) {
- if (chargs[0]) {
- let cini: any = {};
- const img = <HTMLImageElement>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 = <HTMLSelectElement>document.getElementById('mute_select');
- mute_select.add(new Option(safeTags(chargs[0]), String(charid)));
- const pair_select = <HTMLSelectElement>document.getElementById('pair_select');
- pair_select.add(new Option(safeTags(chargs[0]), String(charid)));
-
- // sometimes ini files lack important settings
- const default_options = {
- name: chargs[0],
- showname: chargs[0],
- side: 'def',
- blips: 'male',
- chat: '',
- category: '',
- };
- cini.options = Object.assign(default_options, cini.options);
-
- // sometimes ini files lack important settings
- const default_emotions = {
- number: 0,
- };
- cini.emotions = Object.assign(default_emotions, cini.emotions);
-
- this.chars[charid] = {
- name: safeTags(chargs[0]),
- showname: safeTags(cini.options.showname),
- desc: safeTags(chargs[1]),
- blips: safeTags(cini.options.blips).toLowerCase(),
- gender: safeTags(cini.options.gender).toLowerCase(),
- side: safeTags(cini.options.side).toLowerCase(),
- chat: (cini.options.chat === '') ? safeTags(cini.options.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 = <HTMLSelectElement>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&&&&&#1#Miles ...
- * @param {Array} args packet arguments
- */
- handleCI(args: string[]) {
- // 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 = Number(args[i - 1]);
- (<HTMLProgressElement>document.getElementById('client_loadingbar')).value = charid;
- setTimeout(() => this.handleCharacterInfo(chargs, charid), 500);
- }
- }
- // Request the next pack
- this.sendServer(`AN#${(Number(args[1]) / 10) + 1}#%`);
- }
-
- /**
- * Handles incoming character information, containing all characters
- * in one packet.
- * @param {Array} args packet arguments
- */
- async handleSC(args: string[]) {
- const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
-
- if (mode === 'watch') { // Spectators don't need to pick a character
- document.getElementById('client_charselect').style.display = 'none';
- } else {
- 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;
- (<HTMLProgressElement>document.getElementById('client_loadingbar')).value = charid;
- 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.
- *
- * EI#id#name&description&type&image&##%
- *
- * @param {Array} args packet arguments
- */
- handleEI(args: string[]) {
- document.getElementById('client_loadingtext').innerHTML = `Loading Evidence ${args[1]}/${this.evidence_list_length}`;
- const evidenceID = Number(args[1]);
- (<HTMLProgressElement>document.getElementById('client_loadingbar')).value = this.char_list_length + evidenceID;
-
- const arg = args[2].split('&');
- this.evidences[evidenceID] = {
- name: prepChat(arg[0]),
- desc: prepChat(arg[1]),
- filename: safeTags(arg[3]),
- icon: `${AO_HOST}evidence/${encodeURI(arg[3].toLowerCase())}`,
- };
-
- this.sendServer('AE'+(evidenceID+1)+'#%');
- }
-
- /**
- * Handles incoming evidence list, all evidences at once
- * item per packet.
- *
- * @param {Array} args packet arguments
- */
- handleLE(args: string[]) {
- this.evidences = [];
- for (let i = 1; i < args.length - 1; i++) {
- (<HTMLProgressElement>document.getElementById('client_loadingbar')).value = this.char_list_length + 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 += `<img src="${this.evidences[i - 1].icon}"
- id="evi_${i}"
- alt="${this.evidences[i - 1].name}"
- class="evi_icon"
- onclick="pickEvidence(${i})">`;
- }
- }
-
resetMusicList() {
this.musics = [];
- document.getElementById('client_musiclist').innerHTML = '';
+ 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 = <HTMLSelectElement>document.getElementById('bg_select');
- bg_select.innerHTML = '';
-
- bg_select.add(new Option('Custom', '0'));
- bg_array.forEach((background: string) => {
- bg_select.add(new Option(background));
- });
- } catch (err) {
- console.warn('there was no backgrounds.json file');
- }
- }
-
- async fetchCharacterList() {
- try {
- const chardata = await request(`${AO_HOST}characters.json`);
- const char_array = JSON.parse(chardata);
- // the try catch will fail before here when there is no file
-
- const char_select = <HTMLSelectElement>document.getElementById('client_ininame');
- char_select.innerHTML = '';
-
- char_array.forEach((character: string) => {
- char_select.add(new Option(character));
- });
- } catch (err) {
- console.warn('there was no characters.json file');
- }
- }
-
- async fetchEvidenceList() {
- try {
- const evidata = await request(`${AO_HOST}evidence.json`);
- const evi_array = JSON.parse(evidata);
- // the try catch will fail before here when there is no file
-
- const evi_select = <HTMLSelectElement>document.getElementById('evi_select');
- evi_select.innerHTML = '';
-
- evi_array.forEach((evi: string) => {
- evi_select.add(new Option(evi));
- });
- evi_select.add(new Option('Custom', '0'));
- } catch (err) {
- console.warn('there was no evidence.json file');
- }
- }
-
- isAudio(trackname: string) {
- const audioEndings = ['.wav', '.mp3', '.ogg', '.opus'];
- return audioEndings.filter((ending) => trackname.endsWith(ending)).length === 1;
- }
-
- addTrack(trackname: string) {
- const newentry = <HTMLOptionElement>document.createElement('OPTION');
- newentry.text = trackname;
- (<HTMLSelectElement>document.getElementById('client_musiclist')).options.add(newentry);
- this.musics.push(trackname);
- }
-
- createArea(id: number, name: string) {
- const thisarea = {
- name,
- players: 0,
- status: 'IDLE',
- cm: '',
- locked: 'FREE',
- };
-
- this.areas.push(thisarea);
-
- // Create area button
- const newarea = document.createElement('SPAN');
- newarea.className = 'area-button area-default';
- newarea.id = `area${id}`;
- newarea.innerText = thisarea.name;
- newarea.title = `Players: ${thisarea.players}\n`
- + `Status: ${thisarea.status}\n`
- + `CM: ${thisarea.cm}\n`
- + `Area lock: ${thisarea.locked}`;
- newarea.onclick = function () {
- area_click(newarea);
- };
+ document.getElementById("areas").innerHTML = "";
- 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);
- }
+ fetchBackgroundList();
+ fetchEvidenceList();
}
- /**
- * Handles incoming music information, containing multiple entries
- * per packet.
- * @param {Array} args packet arguments
- */
- handleEM(args: string[]) {
- 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) {
- const trackname = safeTags(args[i]);
- const trackindex = Number(args[i - 1]);
- (<HTMLProgressElement>document.getElementById('client_loadingbar')).value = this.char_list_length + this.evidence_list_length + trackindex;
- 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#${(Number(args[1]) / 10) + 1}#%`);
- }
-
- /**
- * Handles incoming music information, containing all music in one packet.
- * @param {Array} args packet arguments
- */
- handleSM(args: string[]) {
- 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}`;
- (<HTMLProgressElement>document.getElementById('client_loadingbar')).value = this.char_list_length + this.evidence_list_length + i;
- 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: string[]) {
- 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: string[]) {
- this.resetAreaList();
-
- for (let i = 1; i < args.length - 1; i++) {
- this.createArea(i - 1, safeTags(args[i]));
- }
- }
-
- /**
- * Handles 2 factor auth
- * @param {Array} args packet arguments
- */
- handle2F(_args: string[]) {
- document.getElementById('client_secondfactor').style.display = 'block';
- }
-
- /**
- * Handles the kicked packet
- * @param {string} type is it a kick or a ban
- * @param {string} reason why
- */
- handleBans(type: string, reason: string) {
- document.getElementById('client_error').style.display = 'flex';
- document.getElementById('client_errortext').innerHTML = `${type}:<br>${reason.replace(/\n/g, '<br />')}`;
- (<HTMLElement>document.getElementsByClassName('client_reconnect')[0]).style.display = 'none';
- (<HTMLElement>document.getElementsByClassName('client_reconnect')[1]).style.display = 'none';
- }
-
- /**
- * Handles the kicked packet
- * @param {Array} args kick reason
- */
- handleKK(args: string[]) {
- 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: string[]) {
- 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: string[]) {
- 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: string[]) {
- 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: string[]) {
- document.getElementById('client_loading').style.display = 'none';
- if (mode === 'watch') { // Spectators don't need to pick a character
- document.getElementById('client_waiting').style.display = 'none';
- }
- }
-
- /**
- * Handles a background change.
- * @param {Array} args packet arguments
- */
-
- handleBN(args: string[]) {
- viewport.bgname = safeTags(args[1]);
- const bgfolder = viewport.bgFolder;
- const bg_index = getIndexFromSelect('bg_select', viewport.bgname);
- (<HTMLSelectElement>document.getElementById('bg_select')).selectedIndex = bg_index;
- updateBackgroundPreview();
- if (bg_index === 0) {
- (<HTMLInputElement>document.getElementById('bg_filename')).value = viewport.bgname;
- }
-
- tryUrls(`${AO_HOST}background/${encodeURI(args[1].toLowerCase())}/defenseempty`).then(resp => {(<HTMLImageElement>document.getElementById('bg_preview')).src = resp});
- tryUrls(`${bgfolder}defensedesk`).then((resp) => {(<HTMLImageElement>document.getElementById('client_def_bench')).src = resp});
- tryUrls(`${bgfolder}stand`).then(resp => {(<HTMLImageElement>document.getElementById('client_wit_bench')).src = resp});
- tryUrls(`${bgfolder}prosecutiondesk`).then(resp => {(<HTMLImageElement>document.getElementById('client_pro_bench')).src = resp});
- tryUrls(`${bgfolder}full`).then(resp => {(<HTMLImageElement>document.getElementById('client_court')).src = resp});
- tryUrls(`${bgfolder}defenseempty`).then(resp => {(<HTMLImageElement>document.getElementById('client_court_def')).src = resp});
- tryUrls(`${bgfolder}transition_def`).then(resp => {(<HTMLImageElement>document.getElementById('client_court_deft')).src = resp});
- tryUrls(`${bgfolder}witnessempty`).then(resp => {(<HTMLImageElement>document.getElementById('client_court_wit')).src = resp});
- tryUrls(`${bgfolder}transition_pro`).then(resp => {(<HTMLImageElement>document.getElementById('client_court_prot')).src = resp});
- tryUrls(`${bgfolder}prosecutorempty`).then(resp => {(<HTMLImageElement>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: string[]) {
- const percent_hp = Number(args[2]) * 10;
- let healthbox;
- if (args[1] === '1') {
- // Def hp
- this.hp[0] = Number(args[2]);
- healthbox = document.getElementById('client_defense_hp');
- } else {
- // Pro hp
- this.hp[1] = Number(args[2]);
- healthbox = document.getElementById('client_prosecutor_hp');
- }
- (<HTMLElement>healthbox.getElementsByClassName('health-bar')[0]).style.width = `${percent_hp}%`;
- }
-
- /**
- * Handles a testimony states.
- * @param {Array} args packet arguments
- */
- handleRT(args: string[]) {
- 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: string[]) {
- 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: string[]) {
- 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: string[]) {
- 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: string[]) {
- 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: string[]) {
- this.sendServer('askchaa#%');
- }
-
- /**
- * What? you want a character??
- * @param {Array} args packet arguments
- */
- handleCC(args: string[]) {
- this.sendSelf(`PV#1#CID#${args[2]}#%`);
- }
-
- /**
- * What? you want a character list from me??
- * @param {Array} args packet arguments
- */
- handleaskchaa(_args: string[]) {
- this.sendSelf(`SI#${vanilla_character_arr.length}#0#0#%`);
- }
-
- /**
- * Handle the change of players in an area.
- * @param {Array} args packet arguments
- */
- 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
- 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: string[]) {
- console.info('Server-supported features:');
- console.info(args);
- extrafeatures = args;
-
- if (args.includes('yellowtext')) {
- const colorselect = <HTMLSelectElement>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: 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]);
- this.music_list_length = Number(args[3]);
-
- (<HTMLProgressElement>document.getElementById('client_loadingbar')).max = this.char_list_length + this.evidence_list_length + this.music_list_length;
-
- // 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: string[]) {
- 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: string[]) {
- this.charID = Number(args[3]);
- document.getElementById('client_waiting').style.display = 'none';
- 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 = `<span
- id="emo_0"
- alt="unavailable"
- class="emote_button">No emotes available</span>`;
- } 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
- += `<img src=${emotes[i].button}
- id="emo_${i}"
- alt="${emotes[i].desc}"
- class="emote_button"
- onclick="pickEmotion(${i})">`;
- } 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 = <HTMLSelectElement>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: string) => 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: string[]) {
- AO_HOST = args[1];
- }
-
- /**
- * we are asking ourselves what characters there are
- * @param {Array} args packet arguments
- */
- handleRC(_args: string[]) {
- this.sendSelf(`SC#${vanilla_character_arr.join('#')}#%`);
- }
-
- /**
- * we are asking ourselves what characters there are
- * @param {Array} args packet arguments
- */
- handleRM(_args: string[]) {
- this.sendSelf(`SM#${vanilla_music_arr.join('#')}#%`);
- }
-
- /**
- * we are asking ourselves what characters there are
- * @param {Array} args packet arguments
- */
- handleRD(_args: string[]) {
- this.sendSelf('BN#gs4#%');
- this.sendSelf('DONE#%');
- const ooclog = <HTMLInputElement>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;
- theme: string;
-
- constructor() {
- this.textnow = '';
- this.chatmsg = {
- content: '',
- objection: 0,
- sound: '',
- startpreanim: true,
- startspeaking: false,
- side: null,
- color: 0,
- snddelay: 0,
- preanimdelay: 0,
- speed: UPDATE_INTERVAL
- };
-
- 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: HTMLAudioElement) => channel.volume = 0.5);
- this.blipChannels.forEach((channel: HTMLAudioElement) => 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: HTMLAudioElement) => channel.volume = 0.5);
- this.music.forEach((channel: HTMLAudioElement) => 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: number) {
- this.music.forEach((channel: HTMLAudioElement) => 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: string, looping: boolean) {
- 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: string) {
- const bgfolder = viewport.bgFolder;
-
- const view = document.getElementById('client_fullview');
-
- let bench: HTMLImageElement;
- if ('def,pro,wit'.includes(position)) {
- bench = <HTMLImageElement>document.getElementById(`client_${position}_bench`);
- } else {
- bench = <HTMLImageElement>document.getElementById('client_bench_classic');
- }
-
- let court: HTMLImageElement;
- if ('def,pro,wit'.includes(position)) {
- court = <HTMLImageElement>document.getElementById(`client_court_${position}`);
- } else {
- court = <HTMLImageElement>document.getElementById('client_court_classic');
- }
-
- interface Desk {
- ao2?: string
- ao1?: string
- }
- interface Position {
- bg?: string
- desk?: Desk
- speedLines: string
- }
-
- interface Positions {
- [key: string]: Position
- }
-
- const positions: Positions = {
- def: {
- bg: 'defenseempty',
- desk: { ao2: 'defensedesk.png', ao1: 'bancodefensa.png' } as Desk,
- speedLines: 'defense_speedlines.gif',
- },
- pro: {
- bg: 'prosecutorempty',
- desk: { ao2: 'prosecutiondesk.png', ao1: 'bancoacusacion.png' } as Desk,
- speedLines: 'prosecution_speedlines.gif',
- },
- hld: {
- bg: 'helperstand',
- desk: null as Desk,
- speedLines: 'defense_speedlines.gif',
- },
- hlp: {
- bg: 'prohelperstand',
- desk: null as Desk,
- speedLines: 'prosecution_speedlines.gif',
- },
- wit: {
- bg: 'witnessempty',
- desk: { ao2: 'stand.png', ao1: 'estrado.png' } as Desk,
- speedLines: 'prosecution_speedlines.gif',
- },
- jud: {
- bg: 'judgestand',
- desk: { ao2: 'judgedesk.png', ao1: 'judgedesk.gif' } as Desk,
- speedLines: 'prosecution_speedlines.gif',
- },
- jur: {
- bg: 'jurystand',
- desk: { ao2: 'jurydesk.png', ao1: 'estrado.png' } as Desk,
- speedLines: 'defense_speedlines.gif',
- },
- sea: {
- bg: 'seancestand',
- desk: { ao2: 'seancedesk.png', ao1: 'estrado.png' } as Desk,
- speedLines: 'prosecution_speedlines.gif',
- },
- };
-
- 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: Testimony = {
- 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 = <HTMLImageElement>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: Testimony = {
- 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: any) {
-
- 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 = <HTMLImageElement>document.getElementById('client_fg');
- const gamewindow = document.getElementById('client_gamewindow');
- const waitingBox = document.getElementById('client_chatwaiting');
-
- // Reset CSS animation
- gamewindow.style.animation = '';
- waitingBox.style.opacity = '0';
-
- const eviBox = document.getElementById('client_evi');
-
- if (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 = ((<HTMLInputElement>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 = <HTMLImageElement>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;
- chatContainerBox.style.opacity = '1';
- }
- 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: HTMLAudioElement) => 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();
- }
-
- async handleTextTick(charLayers: HTMLImageElement) {
- 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) {
- 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
- }))
- const textSpeeds = new Set(['{', '}'])
-
- // Changing Text Speed
- if (textSpeeds.has(characterElement.innerHTML)) {
- // Grab them all in a row
- const MAX_SLOW_CHATSPEED = 120
- for(let i = 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 === '}') {
- 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);
- 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)()
- } else {
- 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`!
- */
- async tick() {
- await delay(this.chatmsg.speed)
-
- if (this.textnow === this.chatmsg.content) {
- return
- }
-
- const gamewindow = document.getElementById('client_gamewindow');
- const waitingBox = document.getElementById('client_chatwaiting');
- const eviBox = <HTMLImageElement>document.getElementById('client_evi');
- const shoutSprite = <HTMLImageElement>document.getElementById('client_shout');
- const effectlayer = <HTMLImageElement>document.getElementById('client_fg');
- const chatBoxInner = document.getElementById('client_inner_chat');
- let charLayers = <HTMLImageElement>document.getElementById('client_char');
- let pairLayers = <HTMLImageElement>document.getElementById('client_pair_char');
-
- if ('def,pro,wit'.includes(this.chatmsg.side)) {
- charLayers = <HTMLImageElement>document.getElementById(`client_${this.chatmsg.side}_char`);
- pairLayers = <HTMLImageElement>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';
- await 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);
- return
- }
- } else if (this.textnow !== this.chatmsg.content) {
- await 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);
- }
- }
- if (this._animating) {
- this.tick()
- }
- 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: KeyboardEvent) {
- if (event.keyCode === 13) {
- client.sendOOC((<HTMLInputElement>document.getElementById('client_oocinputbox')).value);
- (<HTMLInputElement>document.getElementById('client_oocinputbox')).value = '';
- }
-}
-window.onOOCEnter = onOOCEnter;
-
-/**
- * Triggered when the user click replay GOOOOO
- * @param {KeyboardEvent} event
- */
-export function onReplayGo(_event: 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: KeyboardEvent) {
- if (event.keyCode === 13) {
- const mychar = client.character;
- const myemo = client.emote;
- const evi = client.evidence;
- 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(((<HTMLInputElement>document.getElementById('check_nonint')).checked));
- const looping_sfx = Boolean(((<HTMLInputElement>document.getElementById('check_loopsfx')).checked));
- const color = Number((<HTMLInputElement>document.getElementById('textcolor')).value);
- const showname = (<HTMLInputElement>document.getElementById('ic_chat_name')).value;
- const text = (<HTMLInputElement>document.getElementById('client_inputbox')).value;
- const pairchar = (<HTMLInputElement>document.getElementById('pair_select')).value;
- const pairoffset = Number((<HTMLInputElement>document.getElementById('pair_offset')).value);
- const pairyoffset = Number((<HTMLInputElement>document.getElementById('pair_y_offset')).value);
- const myrole = (<HTMLInputElement>document.getElementById('role_select')).value ? (<HTMLInputElement>document.getElementById('role_select')).value : mychar.side;
- const additive = Boolean(((<HTMLInputElement>document.getElementById('check_additive')).checked));
- const effect = (<HTMLInputElement>document.getElementById('effect_select')).value;
-
- let sfxname = '0';
- let sfxdelay = 0;
- let emote_mod = myemo.zoom;
- if ((<HTMLInputElement>document.getElementById('sendsfx')).checked) {
- sfxname = myemo.sfx;
- sfxdelay = myemo.sfxdelay;
- }
-
- // not to overwrite a 5 from the ini or anything else
- if ((<HTMLInputElement>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() {
- (<HTMLInputElement>document.getElementById('client_inputbox')).value = '';
- document.getElementById('button_flash').className = 'client_button';
- document.getElementById('button_shake').className = 'client_button';
-
- (<HTMLInputElement>document.getElementById('sendpreanim')).checked = false;
-
- if (selectedShout) {
- document.getElementById(`button_${selectedShout}`).className = 'client_button';
- selectedShout = 0;
- }
-}
-
-export function resetOffset(_event: Event) {
- (<HTMLInputElement>document.getElementById('pair_offset')).value = '0';
- (<HTMLInputElement>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: Event) {
- const musiclist_element = <HTMLSelectElement>document.getElementById('client_musiclist');
- const searchname = (<HTMLInputElement>document.getElementById('client_musicsearch')).value;
-
- musiclist_element.innerHTML = '';
-
- for (const trackname of client.musics) {
- if (trackname.toLowerCase().indexOf(searchname.toLowerCase()) !== -1) {
- const newentry = <HTMLOptionElement>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: Event) {
- const playtrack = (<HTMLInputElement>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 = (<HTMLSelectElement>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: Event) {
- const mutelist = <HTMLSelectElement>document.getElementById('mute_select');
- const selected_character = mutelist.options[mutelist.selectedIndex];
-
- if (client.chars[selected_character.value].muted === false) {
- client.chars[selected_character.value].muted = true;
- selected_character.text = `${client.chars[selected_character.value].name} (muted)`;
- console.info(`muted ${client.chars[selected_character.value].name}`);
- } else {
- client.chars[selected_character.value].muted = false;
- selected_character.text = client.chars[selected_character.value].name;
- }
-}
-window.mutelist_click = mutelist_click;
-
-/**
- * Triggered when the showname checkboc is clicked
- * @param {MouseEvent} event
- */
-export function showname_click(_event: Event) {
- setCookie('showname', String((<HTMLInputElement>document.getElementById('showname')).checked));
- setCookie('ic_chat_name', (<HTMLInputElement>document.getElementById('ic_chat_name')).value);
-
- const css_s = <HTMLAnchorElement>document.getElementById('nameplate_setting');
-
- if ((<HTMLInputElement>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 {HTMLElement} el
- */
-export function area_click(el: HTMLElement) {
- 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 = Number((<HTMLInputElement>document.getElementById('client_mvolume')).value);
- setCookie('musicVolume', String(viewport.musicVolume));
-}
-window.changeMusicVolume = changeMusicVolume;
-
-/**
- * Triggered by the blip volume slider.
- */
-export function changeBlipVolume() {
- const blipVolume = (<HTMLInputElement>document.getElementById('client_bvolume')).value;
- viewport.blipChannels.forEach((channel: HTMLAudioElement) => channel.volume = Number(blipVolume));
- setCookie('blipVolume', blipVolume);
-}
-window.changeBlipVolume = changeBlipVolume;
-
-/**
- * Triggered by the theme selector.
- */
-export function reloadTheme() {
- viewport.theme = (<HTMLSelectElement>document.getElementById('client_themeselect')).value;
- setCookie('theme', viewport.theme);
- (<HTMLAnchorElement>document.getElementById('client_theme')).href = `styles/${viewport.theme}.css`;
-}
-window.reloadTheme = reloadTheme;
-
-/**
- * Triggered by a changed callword list
- */
-export function changeCallwords() {
- client.callwords = (<HTMLInputElement>document.getElementById('client_callwords')).value.split('\n');
- setCookie('callwords', client.callwords.join('\n'));
-}
-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 = (<HTMLInputElement>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: number) {
- const background = document.getElementById('client_fullview');
- if (addcheck === 1) {
- (<HTMLInputElement>document.getElementById('client_pantilt')).checked = true;
- document.getElementById('client_court').style.display = '';
- } else if (addcheck === 2) {
- (<HTMLInputElement>document.getElementById('client_pantilt')).checked = false;
- document.getElementById('client_court').style.display = 'none';
- }
- if ((<HTMLInputElement>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 = <HTMLInputElement>document.getElementById('client_hdviewport_offset');
- if ((<HTMLInputElement>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 ((<HTMLInputElement>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: Event) {
- document.getElementById('client_waiting').style.display = 'block';
- 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: HTMLImageElement) {
- 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: HTMLImageElement) {
- image.onerror = null;
- 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: HTMLAudioElement): OnErrorEventHandlerNonNull{
- 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) {
- 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: string, showname = '', nameplate = '', time = new Date()) {
- const entry = document.createElement('p');
- const shownameField = document.createElement('span');
- const nameplateField = document.createElement('span');
- const textField = document.createElement('span');
- nameplateField.className = 'iclog_name iclog_nameplate';
- nameplateField.appendChild(document.createTextNode(nameplate));
-
- shownameField.className = 'iclog_name iclog_showname';
- if (showname === '' || !showname) { shownameField.appendChild(document.createTextNode(nameplate)); } else { shownameField.appendChild(document.createTextNode(showname)); }
-
- textField.className = 'iclog_text';
- textField.appendChild(document.createTextNode(msg));
-
- entry.appendChild(shownameField);
- entry.appendChild(nameplateField);
- entry.appendChild(textField);
-
- // Only put a timestamp if the minute has changed.
- if (lastICMessageTime.getMinutes() !== time.getMinutes()) {
- const timeStamp = document.createElement('span');
- timeStamp.className = 'iclog_time';
- timeStamp.innerText = time.toLocaleTimeString(undefined, {
- hour: 'numeric',
- minute: '2-digit',
- });
- entry.appendChild(timeStamp);
- }
-
- const clientLog = document.getElementById('client_log');
- clientLog.appendChild(entry);
-
- /* This is a little buggy - some troubleshooting might be desirable */
- if (clientLog.scrollTop > clientLog.scrollHeight - 800) {
- clientLog.scrollTop = clientLog.scrollHeight;
- }
-
- lastICMessageTime = new Date();
-}
-
-/**
- * check if the message contains an entry on our callword list
- * @param {string} message
- */
-export function checkCallword(message: string) {
- client.callwords.forEach(testCallword);
-
- function testCallword(item: string) {
- 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: Event) {
- const searchname = (<HTMLInputElement>document.getElementById('client_charactersearch')).value;
-
- 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';
- } 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: number) {
- if (ccharacter === -1) {
- // Spectator
- document.getElementById('client_waiting').style.display = 'none';
- 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: number) {
- 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';
-
- (<HTMLInputElement>document.getElementById('sendsfx')).checked = (client.emote.sfx.length > 1);
-
- (<HTMLInputElement>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
- (<HTMLInputElement>document.getElementById('evi_name')).value = client.evidences[evidence - 1].name;
- (<HTMLInputElement>document.getElementById('evi_desc')).value = client.evidences[evidence - 1].desc;
-
- // Update icon
- const icon_id = getIndexFromSelect('evi_select', client.evidences[evidence - 1].filename);
- (<HTMLSelectElement>document.getElementById('evi_select')).selectedIndex = icon_id;
- if (icon_id === 0) {
- (<HTMLInputElement>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 = <HTMLSelectElement>document.getElementById('evi_select');
- client.sendPE(
- (<HTMLInputElement>document.getElementById('evi_name')).value,
- (<HTMLInputElement>document.getElementById('evi_desc')).value,
- evidence_select.selectedIndex === 0
- ? (<HTMLInputElement>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 = <HTMLSelectElement>document.getElementById('evi_select');
- const id = client.selectedEvidence - 1;
- client.sendEE(
- id,
- (<HTMLInputElement>document.getElementById('evi_name')).value,
- (<HTMLInputElement>document.getElementById('evi_desc')).value,
- evidence_select.selectedIndex === 0
- ? (<HTMLInputElement>document.getElementById('evi_filename')).value
- : evidence_select.options[evidence_select.selectedIndex].text,
- );
- cancelEvidence();
-}
-window.editEvidence = editEvidence;
-
-/**
- * Delete selected evidence.
- */
-export function deleteEvidence() {
- const id = 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
- (<HTMLSelectElement>document.getElementById('evi_select')).selectedIndex = 0;
- updateEvidenceIcon(); // Update icon widget
- (<HTMLInputElement>document.getElementById('evi_filename')).value = '';
- (<HTMLInputElement>document.getElementById('evi_name')).value = '';
- (<HTMLInputElement>document.getElementById('evi_desc')).value = '';
- (<HTMLImageElement>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 = <HTMLSelectElement>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 = <HTMLAnchorElement>document.getElementById('chatbox_theme');
- const themeselect = <HTMLSelectElement>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 = <HTMLSelectElement>document.getElementById('evi_select');
- const evidence_filename = <HTMLInputElement>document.getElementById('evi_filename');
- const evidence_iconbox = <HTMLImageElement>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 = <HTMLSelectElement>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 = <HTMLSelectElement>document.getElementById('bg_select');
- const changeBGCommand = "bg $1";
- const bgFilename = <HTMLInputElement>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 = <HTMLInputElement>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, (client.hp[0] + 1));
-}
-window.addHPD = addHPD;
-
-/**
- * Decrement defense health point.
- */
-export function redHPD() {
- client.sendHP(1, (client.hp[0] - 1));
-}
-window.redHPD = redHPD;
-
-/**
- * Increment prosecution health point.
- */
-export function addHPP() {
- client.sendHP(2, (client.hp[1] + 1));
-}
-window.addHPP = addHPP;
-
-/**
- * Decrement prosecution health point.
- */
-export function redHPP() {
- client.sendHP(2, (client.hp[1] - 1));
-}
-window.redHPP = redHPP;
-
-/**
- * Update background preview.
- */
-export function updateBackgroundPreview() {
- const background_select = <HTMLSelectElement>document.getElementById('bg_select');
- const background_filename = <HTMLInputElement>document.getElementById('bg_filename');
- const background_preview = <HTMLImageElement>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;
-
-function handleCredentialResponse(response: any) {
- client.sendServer(`2T#${response.credential}#%`);
}
-window.handleCredentialResponse = handleCredentialResponse;
-export default Client \ No newline at end of file
+export default Client;
diff --git a/webAO/client/__tests__/setEmote.test.js b/webAO/client/__tests__/setEmote.test.js
index 1db13c9..53bb68d 100644
--- a/webAO/client/__tests__/setEmote.test.js
+++ b/webAO/client/__tests__/setEmote.test.js
@@ -9,10 +9,13 @@ jest.mock('../../utils/fileExists');
describe('setEmote', () => {
const AO_HOST = '';
Client.mockReturnValue({
- lastChar: 'long',
- chatmsg: {
- name: 'byte',
- },
+ viewport: {
+ lastChar: 'long',
+ chatmsg: {
+ name: 'byte',
+ },
+ }
+
});
const client = new Client('127.0.0.1');
const firstExtension = '.gif';
diff --git a/webAO/client/addTrack.ts b/webAO/client/addTrack.ts
new file mode 100644
index 0000000..247f07e
--- /dev/null
+++ b/webAO/client/addTrack.ts
@@ -0,0 +1,15 @@
+import { client } from "../client";
+import { unescapeChat } from "../encoding";
+import { getFilenameFromPath } from "../utils/paths";
+
+
+export const addTrack = (trackname: string) => {
+ const newentry = <HTMLOptionElement>document.createElement("OPTION");
+ const songName = getFilenameFromPath(trackname);
+ newentry.text = unescapeChat(songName);
+ newentry.value = trackname;
+ (<HTMLSelectElement>(
+ document.getElementById("client_musiclist")
+ )).options.add(newentry);
+ client.musics.push(trackname);
+} \ No newline at end of file
diff --git a/webAO/client/aoHost.js b/webAO/client/aoHost.ts
index b387608..9b0a768 100644
--- a/webAO/client/aoHost.js
+++ b/webAO/client/aoHost.ts
@@ -1,5 +1,7 @@
import queryParser from '../utils/queryParser'
let { asset } = queryParser();
const DEFAULT_HOST = 'http://attorneyoffline.de/base/';
-const AO_HOST = asset || DEFAULT_HOST
-export default AO_HOST
+export let AO_HOST = asset || DEFAULT_HOST
+export const setAOhost = (val: string) => {
+ AO_HOST = val
+}
diff --git a/webAO/client/appendICLog.ts b/webAO/client/appendICLog.ts
new file mode 100644
index 0000000..f8b7852
--- /dev/null
+++ b/webAO/client/appendICLog.ts
@@ -0,0 +1,57 @@
+import { lastICMessageTime, setLastICMessageTime } from "../client";
+
+
+
+/**
+ * Appends a message to the in-character chat log.
+ * @param {string} msg the string to be added
+ * @param {string} name the name of the sender
+ */
+export function appendICLog(
+ msg: string,
+ showname = "",
+ nameplate = "",
+ time = new Date()
+) {
+ const entry = document.createElement("p");
+ const shownameField = document.createElement("span");
+ const nameplateField = document.createElement("span");
+ const textField = document.createElement("span");
+ nameplateField.className = "iclog_name iclog_nameplate";
+ nameplateField.appendChild(document.createTextNode(nameplate));
+
+ shownameField.className = "iclog_name iclog_showname";
+ if (showname === "" || !showname) {
+ shownameField.appendChild(document.createTextNode(nameplate));
+ } else {
+ shownameField.appendChild(document.createTextNode(showname));
+ }
+
+ textField.className = "iclog_text";
+ textField.appendChild(document.createTextNode(msg));
+
+ entry.appendChild(shownameField);
+ entry.appendChild(nameplateField);
+ entry.appendChild(textField);
+
+ // Only put a timestamp if the minute has changed.
+ if (lastICMessageTime.getMinutes() !== time.getMinutes()) {
+ const timeStamp = document.createElement("span");
+ timeStamp.className = "iclog_time";
+ timeStamp.innerText = time.toLocaleTimeString(undefined, {
+ hour: "numeric",
+ minute: "2-digit",
+ });
+ entry.appendChild(timeStamp);
+ }
+
+ const clientLog = document.getElementById("client_log")!;
+ clientLog.appendChild(entry);
+
+ /* This is a little buggy - some troubleshooting might be desirable */
+ if (clientLog.scrollTop > clientLog.scrollHeight - 800) {
+ clientLog.scrollTop = clientLog.scrollHeight;
+ }
+
+ setLastICMessageTime(new Date());
+} \ No newline at end of file
diff --git a/webAO/client/checkCallword.ts b/webAO/client/checkCallword.ts
new file mode 100644
index 0000000..f6cffbc
--- /dev/null
+++ b/webAO/client/checkCallword.ts
@@ -0,0 +1,17 @@
+import { client } from "../client";
+import { AO_HOST } from "./aoHost";
+
+/**
+ * check if the message contains an entry on our callword list
+ * @param {string} message
+ */
+export function checkCallword(message: string, sfxAudio: HTMLAudioElement) {
+ client.callwords.forEach(testCallword);
+ function testCallword(item: string) {
+ if (item !== "" && message.toLowerCase().includes(item.toLowerCase())) {
+ sfxAudio.pause();
+ sfxAudio.src = `${AO_HOST}sounds/general/sfx-gallery.opus`;
+ sfxAudio.play();
+ }
+ }
+} \ No newline at end of file
diff --git a/webAO/client/createArea.ts b/webAO/client/createArea.ts
new file mode 100644
index 0000000..63af644
--- /dev/null
+++ b/webAO/client/createArea.ts
@@ -0,0 +1,30 @@
+import { client } from "../client";
+import { area_click } from "../dom/areaClick";
+
+export const createArea = (id: number, name: string) => {
+ const thisarea = {
+ name,
+ players: 0,
+ status: "IDLE",
+ cm: "",
+ locked: "FREE",
+ };
+
+ client.areas.push(thisarea);
+
+ // Create area button
+ const newarea = document.createElement("SPAN");
+ newarea.className = "area-button area-default";
+ newarea.id = `area${id}`;
+ newarea.innerText = thisarea.name;
+ newarea.title =
+ `Players: ${thisarea.players}\n` +
+ `Status: ${thisarea.status}\n` +
+ `CM: ${thisarea.cm}\n` +
+ `Area lock: ${thisarea.locked}`;
+ newarea.onclick = function () {
+ area_click(newarea);
+ };
+
+ document.getElementById("areas")!.appendChild(newarea);
+} \ No newline at end of file
diff --git a/webAO/client/fetchLists.ts b/webAO/client/fetchLists.ts
new file mode 100644
index 0000000..e9772cb
--- /dev/null
+++ b/webAO/client/fetchLists.ts
@@ -0,0 +1,60 @@
+import { AO_HOST } from "./aoHost";
+import { request } from "../services/request.js";
+
+export const fetchBackgroundList = async () => {
+ try {
+ const bgdata = await request(`${AO_HOST}backgrounds.json`);
+ const bg_array = JSON.parse(bgdata);
+ // the try catch will fail before here when there is no file
+
+ const bg_select = <HTMLSelectElement>document.getElementById("bg_select");
+ bg_select.innerHTML = "";
+
+ bg_select.add(new Option("Custom", "0"));
+ bg_array.forEach((background: string) => {
+ bg_select.add(new Option(background));
+ });
+ } catch (err) {
+ console.warn("there was no backgrounds.json file");
+ }
+}
+
+export const fetchCharacterList = async () => {
+ try {
+ const chardata = await request(`${AO_HOST}characters.json`);
+ const char_array = JSON.parse(chardata);
+ // the try catch will fail before here when there is no file
+
+ const char_select = <HTMLSelectElement>(
+ document.getElementById("client_ininame")
+ );
+ char_select.innerHTML = "";
+
+ char_array.forEach((character: string) => {
+ char_select.add(new Option(character));
+ });
+ } catch (err) {
+ console.warn("there was no characters.json file");
+ }
+}
+
+
+export const fetchEvidenceList = async () => {
+ try {
+ const evidata = await request(`${AO_HOST}evidence.json`);
+ const evi_array = JSON.parse(evidata);
+ // the try catch will fail before here when there is no file
+
+ const evi_select = <HTMLSelectElement>(
+ document.getElementById("evi_select")
+ );
+ evi_select.innerHTML = "";
+
+ evi_array.forEach((evi: string) => {
+ evi_select.add(new Option(evi));
+ });
+ evi_select.add(new Option("Custom", "0"));
+ } catch (err) {
+ console.warn("there was no evidence.json file");
+ }
+} \ No newline at end of file
diff --git a/webAO/client/fixLastArea.ts b/webAO/client/fixLastArea.ts
new file mode 100644
index 0000000..f1aa99f
--- /dev/null
+++ b/webAO/client/fixLastArea.ts
@@ -0,0 +1,15 @@
+import { client } from "../client";
+import { addTrack } from "./addTrack";
+
+
+/**
+ * Area list fuckery
+ */
+export const fix_last_area = () => {
+ if (client.areas.length > 0) {
+ const malplaced = client.areas.pop().name;
+ const areas = document.getElementById("areas")!;
+ areas.removeChild(areas.lastChild);
+ addTrack(malplaced);
+ }
+} \ No newline at end of file
diff --git a/webAO/client/handleBans.ts b/webAO/client/handleBans.ts
new file mode 100644
index 0000000..b977fc8
--- /dev/null
+++ b/webAO/client/handleBans.ts
@@ -0,0 +1,17 @@
+/**
+ * Handles the kicked packet
+ * @param {string} type is it a kick or a ban
+ * @param {string} reason why
+ */
+export const handleBans = (type: string, reason: string) => {
+ document.getElementById("client_error")!.style.display = "flex";
+ document.getElementById(
+ "client_errortext"
+ )!.innerHTML = `${type}:<br>${reason.replace(/\n/g, "<br />")}`;
+ (<HTMLElement>(
+ document.getElementsByClassName("client_reconnect")[0]
+ )).style.display = "none";
+ (<HTMLElement>(
+ document.getElementsByClassName("client_reconnect")[1]
+ )).style.display = "none";
+} \ No newline at end of file
diff --git a/webAO/client/handleCharacterInfo.ts b/webAO/client/handleCharacterInfo.ts
new file mode 100644
index 0000000..9d74a8b
--- /dev/null
+++ b/webAO/client/handleCharacterInfo.ts
@@ -0,0 +1,105 @@
+import { client } from "../client";
+import { safeTags } from "../encoding";
+import iniParse from "../iniParse";
+import request from "../services/request";
+import fileExists from "../utils/fileExists";
+import { AO_HOST } from "./aoHost";
+
+
+/**
+ * Handles the incoming character information, and downloads the sprite + ini for it
+ * @param {Array} chargs packet arguments
+ * @param {Number} charid character ID
+ */
+export const handleCharacterInfo = async (chargs: string[], charid: number) => {
+ const img = <HTMLImageElement>document.getElementById(`demo_${charid}`);
+ if (chargs[0]) {
+ let cini: any = {};
+ const getCharIcon = async () => {
+ const extensions = [".png", ".webp"];
+ img.alt = chargs[0];
+ const charIconBaseUrl = `${AO_HOST}characters/${encodeURI(
+ chargs[0].toLowerCase()
+ )}/char_icon`;
+ for (let i = 0; i < extensions.length; i++) {
+ const fileUrl = charIconBaseUrl + extensions[i];
+ const exists = await fileExists(fileUrl);
+ if (exists) {
+ img.alt = chargs[0];
+ img.title = chargs[0];
+ img.src = fileUrl;
+ return;
+ }
+ }
+ };
+ getCharIcon();
+
+ // If the ini doesn't exist on the server this will throw an error
+ try {
+ const cinidata = await request(
+ `${AO_HOST}characters/${encodeURI(chargs[0].toLowerCase())}/char.ini`
+ );
+ cini = iniParse(cinidata);
+ } catch (err) {
+ cini = {};
+ img.classList.add("noini");
+ console.warn(`character ${chargs[0]} is missing from webAO`);
+ // If it does, give the user a visual indication that the character is unusable
+ }
+
+ const mute_select = <HTMLSelectElement>(
+ document.getElementById("mute_select")
+ );
+ mute_select.add(new Option(safeTags(chargs[0]), String(charid)));
+ const pair_select = <HTMLSelectElement>(
+ document.getElementById("pair_select")
+ );
+ pair_select.add(new Option(safeTags(chargs[0]), String(charid)));
+
+ // sometimes ini files lack important settings
+ const default_options = {
+ name: chargs[0],
+ showname: chargs[0],
+ side: "def",
+ blips: "male",
+ chat: "",
+ category: "",
+ };
+ cini.options = Object.assign(default_options, cini.options);
+
+ // sometimes ini files lack important settings
+ const default_emotions = {
+ number: 0,
+ };
+ cini.emotions = Object.assign(default_emotions, cini.emotions);
+
+ client.chars[charid] = {
+ name: safeTags(chargs[0]),
+ showname: safeTags(cini.options.showname),
+ desc: safeTags(chargs[1]),
+ blips: safeTags(cini.options.blips).toLowerCase(),
+ gender: safeTags(cini.options.gender).toLowerCase(),
+ side: safeTags(cini.options.side).toLowerCase(),
+ chat:
+ cini.options.chat === ""
+ ? safeTags(cini.options.category).toLowerCase()
+ : safeTags(cini.options.chat).toLowerCase(),
+ evidence: chargs[3],
+ icon: img.src,
+ inifile: cini,
+ muted: false,
+ };
+
+ if (
+ client.chars[charid].blips === "male" &&
+ client.chars[charid].gender !== "male" &&
+ client.chars[charid].gender !== ""
+ ) {
+ client.chars[charid].blips = client.chars[charid].gender;
+ }
+
+ } else {
+ console.warn(`missing charid ${charid}`);
+ img.style.display = "none";
+ }
+} \ No newline at end of file
diff --git a/webAO/client/isAudio.ts b/webAO/client/isAudio.ts
new file mode 100644
index 0000000..430f543
--- /dev/null
+++ b/webAO/client/isAudio.ts
@@ -0,0 +1,6 @@
+export const isAudio = (trackname: string) => {
+ const audioEndings = [".wav", ".mp3", ".ogg", ".opus"];
+ return (
+ audioEndings.filter((ending) => trackname.endsWith(ending)).length === 1
+ );
+} \ No newline at end of file
diff --git a/webAO/client/isLowMemory.ts b/webAO/client/isLowMemory.ts
new file mode 100644
index 0000000..caa6784
--- /dev/null
+++ b/webAO/client/isLowMemory.ts
@@ -0,0 +1,10 @@
+import { setOldLoading } from '../client'
+export const isLowMemory = () => {
+ if (
+ /webOS|iPod|BlackBerry|BB|PlayBook|IEMobile|Windows Phone|Kindle|Silk|PlayStation|Nintendo|Opera Mini/i.test(
+ navigator.userAgent
+ )
+ ) {
+ setOldLoading(true);
+ }
+}
diff --git a/webAO/client/loadResources.ts b/webAO/client/loadResources.ts
new file mode 100644
index 0000000..4954966
--- /dev/null
+++ b/webAO/client/loadResources.ts
@@ -0,0 +1,81 @@
+import getCookie from "../utils/getCookie";
+import vanilla_evidence_arr from "../constants/evidence.js";
+import vanilla_background_arr from "../constants/backgrounds.js";
+import { changeMusicVolume } from '../dom/changeMusicVolume'
+import { setChatbox } from "../dom/setChatbox";
+import { changeSFXVolume, changeShoutVolume, changeTestimonyVolume } from "../dom/changeVolume";
+import { showname_click } from "../dom/showNameClick";
+import { changeBlipVolume } from '../dom/changeBlipVolume'
+import { reloadTheme } from '../dom/reloadTheme'
+const version = process.env.npm_package_version;
+
+/**
+ * Load game resources and stored settings.
+ */
+export const loadResources = () => {
+ document.getElementById("client_version")!.innerText = `version ${version}`;
+ // Load background array to select
+ const background_select = <HTMLSelectElement>(
+ document.getElementById("bg_select")
+ );
+ background_select.add(new Option("Custom", "0"));
+ vanilla_background_arr.forEach((background) => {
+ background_select.add(new Option(background));
+ });
+
+ // Load evidence array to select
+ const evidence_select = <HTMLSelectElement>(
+ document.getElementById("evi_select")
+ );
+ evidence_select.add(new Option("Custom", "0"));
+ vanilla_evidence_arr.forEach((evidence) => {
+ evidence_select.add(new Option(evidence));
+ });
+
+ // Read cookies and set the UI to its values
+ (<HTMLInputElement>document.getElementById("OOC_name")).value =
+ getCookie("OOC_name") ||
+ `web${String(Math.round(Math.random() * 100 + 10))}`;
+
+ // Read cookies and set the UI to its values
+ const cookietheme = getCookie("theme") || "default";
+
+ (<HTMLOptionElement>(
+ document.querySelector(`#client_themeselect [value="${cookietheme}"]`)
+ )).selected = true;
+ reloadTheme();
+
+ const cookiechatbox = getCookie("chatbox") || "dynamic";
+
+ (<HTMLOptionElement>(
+ document.querySelector(`#client_chatboxselect [value="${cookiechatbox}"]`)
+ )).selected = true;
+ setChatbox(cookiechatbox);
+
+ (<HTMLInputElement>document.getElementById("client_mvolume")).value =
+ getCookie("musicVolume") || "1";
+ changeMusicVolume();
+ (<HTMLAudioElement>document.getElementById("client_sfxaudio")).volume =
+ Number(getCookie("sfxVolume")) || 1;
+ changeSFXVolume();
+ (<HTMLAudioElement>document.getElementById("client_shoutaudio")).volume =
+ Number(getCookie("shoutVolume")) || 1;
+ changeShoutVolume();
+ (<HTMLAudioElement>(
+ document.getElementById("client_testimonyaudio")
+ )).volume = Number(getCookie("testimonyVolume")) || 1;
+ changeTestimonyVolume();
+ (<HTMLInputElement>document.getElementById("client_bvolume")).value =
+ getCookie("blipVolume") || "1";
+ changeBlipVolume();
+
+ (<HTMLInputElement>document.getElementById("ic_chat_name")).value =
+ getCookie("ic_chat_name");
+ (<HTMLInputElement>document.getElementById("showname")).checked = Boolean(
+ getCookie("showname")
+ );
+ showname_click(null);
+
+ (<HTMLInputElement>document.getElementById("client_callwords")).value =
+ getCookie("callwords");
+} \ No newline at end of file
diff --git a/webAO/client/resetICParams.ts b/webAO/client/resetICParams.ts
new file mode 100644
index 0000000..414da27
--- /dev/null
+++ b/webAO/client/resetICParams.ts
@@ -0,0 +1,21 @@
+import { selectedShout, setSelectedShout } from "../client";
+
+/**
+ * Resets the IC parameters for the player to enter a new chat message.
+ * This should only be called when the player's previous chat message
+ * was successfully sent/presented.
+ */
+export function resetICParams() {
+ (<HTMLInputElement>document.getElementById("client_inputbox")).value = "";
+ document.getElementById("button_flash")!.className = "client_button";
+ document.getElementById("button_shake")!.className = "client_button";
+
+ (<HTMLInputElement>document.getElementById("sendpreanim")).checked = false;
+ (<HTMLInputElement>document.getElementById("sendsfx")).checked = false;
+
+ if (selectedShout) {
+ document.getElementById(`button_${selectedShout}`)!.className =
+ "client_button";
+ setSelectedShout(0);
+ }
+} \ No newline at end of file
diff --git a/webAO/client/saveChatLogHandle.ts b/webAO/client/saveChatLogHandle.ts
new file mode 100644
index 0000000..bcc1075
--- /dev/null
+++ b/webAO/client/saveChatLogHandle.ts
@@ -0,0 +1,26 @@
+import downloadFile from "../services/downloadFile";
+
+export const saveChatlogHandle = async () => {
+ const clientLog = document.getElementById("client_log")!;
+ const icMessageLogs = clientLog.getElementsByTagName("p");
+ const messages: string[] = [];
+
+ for (let i = 0; i < icMessageLogs.length; i++) {
+ const SHOWNAME_POSITION = 0;
+ const TEXT_POSITION = 2;
+ const showname = icMessageLogs[i].children[SHOWNAME_POSITION].innerHTML;
+ const text = icMessageLogs[i].children[TEXT_POSITION].innerHTML;
+ const message = `${showname}: ${text}`;
+ messages.push(message);
+ }
+ const d = new Date();
+ let ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(d);
+ let mo = new Intl.DateTimeFormat("en", { month: "short" }).format(d);
+ let da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(d);
+
+ const filename = `chatlog-${da}-${mo}-${ye}`.toLowerCase();
+ downloadFile(messages.join("\n"), filename);
+
+ // Reset Chatbox to Empty
+ (<HTMLInputElement>document.getElementById("client_inputbox")).value = "";
+}; \ No newline at end of file
diff --git a/webAO/client/sender/index.ts b/webAO/client/sender/index.ts
new file mode 100644
index 0000000..41a6bd5
--- /dev/null
+++ b/webAO/client/sender/index.ts
@@ -0,0 +1,68 @@
+import { sendIC } from "./sendIC";
+import { sendSelf } from './sendSelf'
+import { sendServer } from './sendServer'
+import { sendCheck } from './sendCheck'
+import {sendHP} from './sendHP'
+import {sendOOC} from './sendOOC'
+import {sendCharacter} from './sendCharacter'
+import {sendRT} from './sendRT'
+import {sendMusicChange} from './sendMusicChange'
+import {sendZZ} from './sendZZ'
+import {sendEE} from './sendEE'
+import {sendDE} from './sendDE'
+import {sendPE} from './sendPE'
+export interface ISender {
+ sendIC: (deskmod: number,
+ preanim: string,
+ name: string,
+ emote: string,
+ message: string,
+ side: string,
+ sfx_name: 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: number,
+ self_yoffset: number,
+ noninterrupting_preanim: boolean,
+ looping_sfx: boolean,
+ screenshake: boolean,
+ frame_screenshake: string,
+ frame_realization: string,
+ frame_sfx: string,
+ additive: boolean,
+ effect: string) => void
+ sendSelf: (message: string) => void
+ sendServer: (message: string) => void
+ sendCheck: () => void
+ sendHP: (side: number, hp: number) => void
+ sendOOC: (message: string) => void
+ sendCharacter: (character: number) => void
+ sendRT: (testimony: string) => void
+ sendMusicChange: (track: string) => void
+ sendZZ: (msg: string) => void
+ sendEE: (id: number, name: string, desc: string, img: string) => void
+ sendDE: (id: number) => void
+ sendPE: (name: string, desc: string, img: string) => void
+}
+export const sender = {
+ sendIC,
+ sendSelf,
+ sendServer,
+ sendCheck,
+ sendHP,
+ sendOOC,
+ sendCharacter,
+ sendRT,
+ sendMusicChange,
+ sendZZ,
+ sendEE,
+ sendDE,
+ sendPE
+} \ No newline at end of file
diff --git a/webAO/client/sender/sendCharacter.ts b/webAO/client/sender/sendCharacter.ts
new file mode 100644
index 0000000..5e81727
--- /dev/null
+++ b/webAO/client/sender/sendCharacter.ts
@@ -0,0 +1,11 @@
+import { client } from "../../client";
+
+/**
+ * Requests to play as a specified character.
+ * @param {number} character the character ID
+ */
+export const sendCharacter = (character: number) => {
+ if (character === -1 || client.chars[character].name) {
+ client.sender.sendServer(`CC#${client.playerID}#${character}#web#%`);
+ }
+} \ No newline at end of file
diff --git a/webAO/client/sender/sendCheck.ts b/webAO/client/sender/sendCheck.ts
new file mode 100644
index 0000000..91b3a02
--- /dev/null
+++ b/webAO/client/sender/sendCheck.ts
@@ -0,0 +1,8 @@
+import { client } from "../../client";
+
+/**
+ * Sends a keepalive packet.
+ */
+export const sendCheck = () => {
+ client.sender.sendServer(`CH#${client.charID}#%`);
+}
diff --git a/webAO/client/sender/sendDE.ts b/webAO/client/sender/sendDE.ts
new file mode 100644
index 0000000..4d94d65
--- /dev/null
+++ b/webAO/client/sender/sendDE.ts
@@ -0,0 +1,9 @@
+import { client } from "../../client";
+
+/**
+ * Sends delete evidence command.
+ * @param {number} evidence id
+ */
+export const sendDE = (id: number) => {
+ client.sender.sendServer(`DE#${id}#%`);
+} \ No newline at end of file
diff --git a/webAO/client/sender/sendEE.ts b/webAO/client/sender/sendEE.ts
new file mode 100644
index 0000000..7c5bfe3
--- /dev/null
+++ b/webAO/client/sender/sendEE.ts
@@ -0,0 +1,16 @@
+import { client } from "../../client";
+import { escapeChat } from "../../encoding";
+
+
+/**
+ * Sends edit evidence command.
+ * @param {number} evidence id
+ * @param {string} evidence name
+ * @param {string} evidence description
+ * @param {string} evidence image filename
+ */
+export const sendEE = (id: number, name: string, desc: string, img: string) => {
+ client.sender.sendServer(
+ `EE#${id}#${escapeChat(name)}#${escapeChat(desc)}#${escapeChat(img)}#%`
+ );
+} \ No newline at end of file
diff --git a/webAO/client/sender/sendHP.ts b/webAO/client/sender/sendHP.ts
new file mode 100644
index 0000000..d007094
--- /dev/null
+++ b/webAO/client/sender/sendHP.ts
@@ -0,0 +1,10 @@
+import { client } from "../../client";
+
+/**
+ * Sends health point command.
+ * @param {number} side the position
+ * @param {number} hp the health point
+ */
+export const sendHP = (side: number, hp: number) => {
+ client.sender.sendServer(`HP#${side}#${hp}#%`);
+} \ No newline at end of file
diff --git a/webAO/client/sender/sendIC.ts b/webAO/client/sender/sendIC.ts
new file mode 100644
index 0000000..9064115
--- /dev/null
+++ b/webAO/client/sender/sendIC.ts
@@ -0,0 +1,106 @@
+import { extrafeatures } from "../../client";
+import { escapeChat } from "../../encoding";
+import {client} from '../../client'
+import queryParser from "../../utils/queryParser";
+let {mode} = queryParser()
+
+/**
+ * Sends an in-character chat message.
+ * @param {number} deskmod controls the desk
+ * @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 {number} emote_modifier whether or not to zoom
+ * @param {number} sfx_delay the delay (in milliseconds) to play the sound effect
+ * @param {number} objection_modifier the number of the shout to play
+ * @param {string} evidence the filename of evidence to show
+ * @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)
+ * @param {number} self_offset offset to paired character (optional)
+ * @param {number} noninterrupting_preanim play the full preanim (optional)
+ */
+export const sendIC = (
+ deskmod: number,
+ preanim: string,
+ name: string,
+ emote: string,
+ message: string,
+ side: string,
+ sfx_name: 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: number,
+ self_yoffset: number,
+ noninterrupting_preanim: boolean,
+ looping_sfx: boolean,
+ screenshake: boolean,
+ frame_screenshake: string,
+ frame_realization: string,
+ frame_sfx: string,
+ additive: boolean,
+ effect: string
+) => {
+ 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}<and>${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 = `${escapeChat(
+ showname
+ )}#${other_charid}${other_emote}#${self_offset}${other_offset}#${Number(
+ noninterrupting_preanim
+ )}#`;
+
+ if (extrafeatures.includes("looping_sfx")) {
+ extra_27 = `${Number(looping_sfx)}#${Number(
+ screenshake
+ )}#${frame_screenshake}#${frame_realization}#${frame_sfx}#`;
+ if (extrafeatures.includes("effects")) {
+ extra_28 = `${Number(additive)}#${escapeChat(effect)}#`;
+ }
+ }
+ }
+
+ const serverMessage =
+ `MS#${deskmod}#${escapeChat(preanim)}#${escapeChat(name)}#${escapeChat(
+ emote
+ )}` +
+ `#${escapeChat(message)}#${escapeChat(side)}#${escapeChat(
+ sfx_name
+ )}#${emote_modifier}` +
+ `#${client.charID}#${sfx_delay}#${Number(objection_modifier)}#${Number(
+ evidence
+ )}#${Number(flip)}#${Number(
+ realization
+ )}#${text_color}#${extra_cccc}${extra_27}${extra_28}%`;
+
+ client.sender.sendServer(serverMessage);
+ if (mode === "replay") {
+ (<HTMLInputElement>(
+ document.getElementById("client_ooclog")
+ )).value += `wait#${(<HTMLInputElement>document.getElementById("client_replaytimer")).value
+ }#%\r\n`;
+ }
+} \ No newline at end of file
diff --git a/webAO/client/sender/sendMusic.ts b/webAO/client/sender/sendMusic.ts
new file mode 100644
index 0000000..eceba08
--- /dev/null
+++ b/webAO/client/sender/sendMusic.ts
@@ -0,0 +1,10 @@
+import { client } from "../../client";
+
+
+/**
+ * Requests to select a music track.
+ * @param {number?} song the song to be played
+ */
+export const sendMusic = (song: string) => {
+ client.sender.sendServer(`MC#${song}#${client.charID}#%`);
+} \ No newline at end of file
diff --git a/webAO/client/sender/sendMusicChange.ts b/webAO/client/sender/sendMusicChange.ts
new file mode 100644
index 0000000..50c6306
--- /dev/null
+++ b/webAO/client/sender/sendMusicChange.ts
@@ -0,0 +1,10 @@
+import { client } from "../../client";
+
+
+/**
+ * Requests to change the music to the specified track.
+ * @param {string} track the track ID
+ */
+export const sendMusicChange = (track: string) => {
+ client.sender.sendServer(`MC#${track}#${client.charID}#%`);
+}
diff --git a/webAO/client/sender/sendOOC.ts b/webAO/client/sender/sendOOC.ts
new file mode 100644
index 0000000..9674ad9
--- /dev/null
+++ b/webAO/client/sender/sendOOC.ts
@@ -0,0 +1,33 @@
+import { client } from '../../client'
+import { escapeChat } from '../../encoding';
+import setCookie from '../../utils/setCookie';
+import { saveChatlogHandle } from '../../client/saveChatLogHandle'
+/**
+ * Sends an out-of-character chat message.
+ * @param {string} message the message to send
+ */
+export const sendOOC = (message: string) => {
+ setCookie(
+ "OOC_name",
+ (<HTMLInputElement>document.getElementById("OOC_name")).value
+ );
+ const oocName = `${escapeChat(
+ (<HTMLInputElement>document.getElementById("OOC_name")).value
+ )}`;
+ const oocMessage = `${escapeChat(message)}`;
+
+ const commands = {
+ "/save_chatlog": 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 {
+ client.sender.sendServer(`CT#${oocName}#${oocMessage}#%`);
+ }
+} \ No newline at end of file
diff --git a/webAO/client/sender/sendPE.ts b/webAO/client/sender/sendPE.ts
new file mode 100644
index 0000000..984fc4d
--- /dev/null
+++ b/webAO/client/sender/sendPE.ts
@@ -0,0 +1,14 @@
+import { client } from "../../client";
+import { escapeChat } from "../../encoding";
+
+/**
+ * Sends add evidence command.
+ * @param {string} evidence name
+ * @param {string} evidence description
+ * @param {string} evidence image filename
+ */
+export const sendPE = (name: string, desc: string, img: string) => {
+ client.sender.sendServer(
+ `PE#${escapeChat(name)}#${escapeChat(desc)}#${escapeChat(img)}#%`
+ );
+} \ No newline at end of file
diff --git a/webAO/client/sender/sendRT.ts b/webAO/client/sender/sendRT.ts
new file mode 100644
index 0000000..2d6c60a
--- /dev/null
+++ b/webAO/client/sender/sendRT.ts
@@ -0,0 +1,9 @@
+import { client } from "../../client";
+
+/**
+ * Sends testimony command.
+ * @param {string} testimony type
+ */
+export const sendRT = (testimony: string) => {
+ client.sender.sendServer(`RT#${testimony}#%`);
+} \ No newline at end of file
diff --git a/webAO/client/sender/sendSelf.ts b/webAO/client/sender/sendSelf.ts
new file mode 100644
index 0000000..66c35fa
--- /dev/null
+++ b/webAO/client/sender/sendSelf.ts
@@ -0,0 +1,13 @@
+import { client } from "../../client";
+
+
+/**
+ * Hook for sending messages to the client
+ * @param {string} message the message to send
+ */
+export const sendSelf = (message: string) => {
+ (<HTMLInputElement>(
+ document.getElementById("client_ooclog")
+ )).value += `${message}\r\n`;
+ client.handleSelf(message);
+} \ No newline at end of file
diff --git a/webAO/client/sender/sendServer.ts b/webAO/client/sender/sendServer.ts
new file mode 100644
index 0000000..a9da3bd
--- /dev/null
+++ b/webAO/client/sender/sendServer.ts
@@ -0,0 +1,10 @@
+import { client } from "../../client";
+import queryParser from "../../utils/queryParser";
+let { mode } = queryParser()
+/**
+ * Hook for sending messages to the server
+ * @param {string} message the message to send
+ */
+export const sendServer = (message: string) => {
+ mode === "replay" ? client.sender.sendSelf(message) : client.serv.send(message);
+} \ No newline at end of file
diff --git a/webAO/client/sender/sendZZ.ts b/webAO/client/sender/sendZZ.ts
new file mode 100644
index 0000000..237ab37
--- /dev/null
+++ b/webAO/client/sender/sendZZ.ts
@@ -0,0 +1,13 @@
+import { client, extrafeatures } from "../../client";
+
+/**
+ * Sends call mod command.
+ * @param {string} message to mod
+ */
+export const sendZZ = (msg: string) => {
+ if (extrafeatures.includes("modcall_reason")) {
+ client.sender.sendServer(`ZZ#${msg}#%`);
+ } else {
+ client.sender.sendServer("ZZ#%");
+ }
+} \ No newline at end of file
diff --git a/webAO/client/setEmote.js b/webAO/client/setEmote.js
deleted file mode 100644
index f682fe5..0000000
--- a/webAO/client/setEmote.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import transparentPng from '../constants/transparentPng';
-import fileExists from '../utils/fileExists';
-
-/**
- * Sets all the img tags to the right sources
- * @param {*} chatmsg
- */
-
-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'];
- const position = acceptedPositions.includes(side) ? `${side}_` : '';
- const emoteSelector = document.getElementById(`client_${position}${pairID}_img`)
- const extensionsMap = [
- '.gif',
- '.png',
- '.apng',
- '.webp'
- ];
-
- for (const extension of extensionsMap) {
- // Hides all sprites before creating a new sprite
- if (client.lastChar !== client.chatmsg.name) {
- emoteSelector.src = transparentPng;
- }
- let url;
- if (extension === '.png') {
- url = `${characterFolder}${encodeURI(charactername)}/${encodeURI(emotename)}${extension}`;
- } else {
- url = `${characterFolder}${encodeURI(charactername)}/${encodeURI(prefix)}${encodeURI(emotename)}${extension}`;
- }
- const exists = await fileExists(url);
- if (exists) {
- emoteSelector.src = url;
- break;
- }
- }
-};
-export default setEmote;
diff --git a/webAO/client/setEmote.ts b/webAO/client/setEmote.ts
new file mode 100644
index 0000000..f4fbdbb
--- /dev/null
+++ b/webAO/client/setEmote.ts
@@ -0,0 +1,55 @@
+import Client from "../client";
+import transparentPng from "../constants/transparentPng";
+import fileExists from "../utils/fileExists";
+
+/**
+ * Sets all the img tags to the right sources
+ * @param {*} chatmsg
+ */
+
+const setEmote = async (
+ AO_HOST: string,
+ client: Client,
+ charactername: string,
+ emotename: string,
+ prefix: string,
+ pair: boolean,
+ side: string
+) => {
+ const pairID = pair ? "pair" : "char";
+ const characterFolder = `${AO_HOST}characters/`;
+ const acceptedPositions = ["def", "pro", "wit"];
+ const position = acceptedPositions.includes(side) ? `${side}_` : "";
+ const emoteSelector = document.getElementById(
+ `client_${position}${pairID}_img`
+ ) as HTMLImageElement;
+ const extensionsMap = [".gif", ".png", ".apng", ".webp", ".webp.static"];
+
+ for (const extension of extensionsMap) {
+ // Hides all sprites before creating a new sprite
+
+ if (client.viewport.getLastCharacter() !== client.viewport.getChatmsg().name) {
+ emoteSelector.src = transparentPng;
+ }
+ let url;
+ if (extension === ".png") {
+ url = `${characterFolder}${encodeURI(charactername)}/${encodeURI(
+ emotename
+ )}${extension}`;
+ } else if (extension === ".webp.static") {
+ url = `${characterFolder}${encodeURI(charactername)}/${encodeURI(
+ emotename
+ )}.webp`;
+ } else {
+ url = `${characterFolder}${encodeURI(charactername)}/${encodeURI(
+ prefix
+ )}${encodeURI(emotename)}${extension}`;
+ }
+ const exists = await fileExists(url);
+ if (exists) {
+ emoteSelector.src = url;
+ break;
+ }
+ }
+};
+export default setEmote;
diff --git a/webAO/components/blip.js b/webAO/components/blip.js
index eacbeaf..db6a784 100644
--- a/webAO/components/blip.js
+++ b/webAO/components/blip.js
@@ -1,4 +1,4 @@
-import AO_HOST from '../client/aoHost'
+import { AO_HOST } from '../client/aoHost'
/**
*
diff --git a/webAO/dom/addEvidence.ts b/webAO/dom/addEvidence.ts
new file mode 100644
index 0000000..8a13f06
--- /dev/null
+++ b/webAO/dom/addEvidence.ts
@@ -0,0 +1,20 @@
+import { client } from "../client";
+import { cancelEvidence } from "./cancelEvidence";
+
+/**
+ * Add evidence.
+ */
+ export function addEvidence() {
+ const evidence_select = <HTMLSelectElement>(
+ document.getElementById("evi_select")
+ );
+ client.sender.sendPE(
+ (<HTMLInputElement>document.getElementById("evi_name")).value,
+ (<HTMLInputElement>document.getElementById("evi_desc")).value,
+ evidence_select.selectedIndex === 0
+ ? (<HTMLInputElement>document.getElementById("evi_filename")).value
+ : evidence_select.options[evidence_select.selectedIndex].text
+ );
+ cancelEvidence();
+ }
+ window.addEvidence = addEvidence; \ No newline at end of file
diff --git a/webAO/dom/addHPD.ts b/webAO/dom/addHPD.ts
new file mode 100644
index 0000000..8f7e1f7
--- /dev/null
+++ b/webAO/dom/addHPD.ts
@@ -0,0 +1,9 @@
+import { client } from "../client";
+
+/**
+ * Increment defense health point.
+ */
+export function addHPD() {
+ client.sender.sendHP(1, client.hp[0] + 1);
+}
+window.addHPD = addHPD; \ No newline at end of file
diff --git a/webAO/dom/addHPP.ts b/webAO/dom/addHPP.ts
new file mode 100644
index 0000000..1379f7c
--- /dev/null
+++ b/webAO/dom/addHPP.ts
@@ -0,0 +1,9 @@
+import { client } from "../client";
+
+/**
+ * Increment prosecution health point.
+ */
+export function addHPP() {
+ client.sender.sendHP(2, client.hp[1] + 1);
+}
+window.addHPP = addHPP; \ No newline at end of file
diff --git a/webAO/dom/areaClick.ts b/webAO/dom/areaClick.ts
new file mode 100644
index 0000000..1b0fe52
--- /dev/null
+++ b/webAO/dom/areaClick.ts
@@ -0,0 +1,15 @@
+import { client } from '../client'
+/**
+ * Triggered when an item on the area list is clicked.
+ * @param {HTMLElement} el
+ */
+export function area_click(el: HTMLElement) {
+ const area = client.areas[el.id.substr(4)].name;
+ client.sender.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; \ No newline at end of file
diff --git a/webAO/dom/callMod.ts b/webAO/dom/callMod.ts
new file mode 100644
index 0000000..a2e2685
--- /dev/null
+++ b/webAO/dom/callMod.ts
@@ -0,0 +1,16 @@
+import { client, extrafeatures } from "../client";
+/**
+ * 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.sender.sendZZ(modcall);
+ }
+}
+window.callMod = callMod; \ No newline at end of file
diff --git a/webAO/dom/cancelEvidence.ts b/webAO/dom/cancelEvidence.ts
new file mode 100644
index 0000000..a906613
--- /dev/null
+++ b/webAO/dom/cancelEvidence.ts
@@ -0,0 +1,36 @@
+import { client, } from "../client";
+import { updateEvidenceIcon } from './updateEvidenceIcon'
+import { AO_HOST } from "../client/aoHost";
+
+
+/**
+ * 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
+ (<HTMLSelectElement>document.getElementById("evi_select")).selectedIndex = 0;
+ updateEvidenceIcon(); // Update icon widget
+ (<HTMLInputElement>document.getElementById("evi_filename")).value = "";
+ (<HTMLInputElement>document.getElementById("evi_name")).value = "";
+ (<HTMLInputElement>document.getElementById("evi_desc")).value = "";
+ (<HTMLImageElement>(
+ 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; \ No newline at end of file
diff --git a/webAO/dom/changeBackgroundOOC.ts b/webAO/dom/changeBackgroundOOC.ts
new file mode 100644
index 0000000..1608ebe
--- /dev/null
+++ b/webAO/dom/changeBackgroundOOC.ts
@@ -0,0 +1,28 @@
+
+import queryParser from '../utils/queryParser'
+import { client } from '../client'
+let { mode } = queryParser()
+
+/**
+ * Change background via OOC.
+ */
+export function changeBackgroundOOC() {
+ const selectedBG = <HTMLSelectElement>document.getElementById("bg_select");
+ const changeBGCommand = "bg $1";
+ const bgFilename = <HTMLInputElement>document.getElementById("bg_filename");
+
+ let filename = "";
+ if (selectedBG.selectedIndex === 0) {
+ filename = bgFilename.value;
+ } else {
+ filename = selectedBG.value;
+ }
+
+
+ if (mode === "join") {
+ client.sender.sendOOC(`/${changeBGCommand.replace("$1", filename)}`);
+ } else if (mode === "replay") {
+ client.sender.sendSelf(`BN#${filename}#%`);
+ }
+}
+window.changeBackgroundOOC = changeBackgroundOOC; \ No newline at end of file
diff --git a/webAO/dom/changeBlipVolume.ts b/webAO/dom/changeBlipVolume.ts
new file mode 100644
index 0000000..572f389
--- /dev/null
+++ b/webAO/dom/changeBlipVolume.ts
@@ -0,0 +1,15 @@
+import setCookie from "../utils/setCookie";
+import { client } from '../client'
+/**
+ * Triggered by the blip volume slider.
+ */
+export const changeBlipVolume = () => {
+ const blipVolume = (<HTMLInputElement>(
+ document.getElementById("client_bvolume")
+ )).value;
+ client.viewport.blipChannels.forEach(
+ (channel: HTMLAudioElement) => (channel.volume = Number(blipVolume))
+ );
+ setCookie("blipVolume", blipVolume);
+}
+window.changeBlipVolume = changeBlipVolume;
diff --git a/webAO/dom/changeCallwords.ts b/webAO/dom/changeCallwords.ts
new file mode 100644
index 0000000..28be674
--- /dev/null
+++ b/webAO/dom/changeCallwords.ts
@@ -0,0 +1,13 @@
+import { client } from '../client'
+import setCookie from '../utils/setCookie';
+
+/**
+ * Triggered by a changed callword list
+ */
+export function changeCallwords() {
+ client.callwords = (<HTMLInputElement>(
+ document.getElementById("client_callwords")
+ )).value.split("\n");
+ setCookie("callwords", client.callwords.join("\n"));
+}
+window.changeCallwords = changeCallwords; \ No newline at end of file
diff --git a/webAO/dom/changeCharacter.ts b/webAO/dom/changeCharacter.ts
new file mode 100644
index 0000000..7ecefe5
--- /dev/null
+++ b/webAO/dom/changeCharacter.ts
@@ -0,0 +1,11 @@
+
+/**
+ * Triggered when a character icon is clicked in the character selection menu.
+ * @param {MouseEvent} event
+ */
+export function changeCharacter(_event: Event) {
+ document.getElementById("client_waiting")!.style.display = "block";
+ document.getElementById("client_charselect")!.style.display = "block";
+ document.getElementById("client_emo")!.innerHTML = "";
+}
+window.changeCharacter = changeCharacter; \ No newline at end of file
diff --git a/webAO/dom/changeMusicVolume.ts b/webAO/dom/changeMusicVolume.ts
new file mode 100644
index 0000000..9e5d51a
--- /dev/null
+++ b/webAO/dom/changeMusicVolume.ts
@@ -0,0 +1,14 @@
+import { client } from '../client'
+import setCookie from '../utils/setCookie';
+
+export const changeMusicVolume = (volume: number = -1) => {
+ const clientVolume = Number(
+ (<HTMLInputElement>document.getElementById("client_mvolume")).value
+ );
+ let musicVolume = volume === -1 ? clientVolume : volume;
+ client.viewport.music.forEach(
+ (channel: HTMLAudioElement) => (channel.volume = musicVolume)
+ );
+ setCookie("musicVolume", String(musicVolume));
+};
+window.changeMusicVolume = changeMusicVolume; \ No newline at end of file
diff --git a/webAO/dom/changeRoleOOC.ts b/webAO/dom/changeRoleOOC.ts
new file mode 100644
index 0000000..7d89bee
--- /dev/null
+++ b/webAO/dom/changeRoleOOC.ts
@@ -0,0 +1,13 @@
+import { updateActionCommands } from './updateActionCommands'
+import { client } from '../client'
+/**
+ * Change role via OOC.
+ */
+export function changeRoleOOC() {
+ const roleselect = <HTMLInputElement>document.getElementById("role_select");
+
+ client.sender.sendOOC(`/pos ${roleselect.value}`);
+ client.sender.sendServer(`SP#${roleselect.value}#%`);
+ updateActionCommands(roleselect.value);
+}
+window.changeRoleOOC = changeRoleOOC; \ No newline at end of file
diff --git a/webAO/dom/charError.ts b/webAO/dom/charError.ts
new file mode 100644
index 0000000..8cb672a
--- /dev/null
+++ b/webAO/dom/charError.ts
@@ -0,0 +1,12 @@
+import transparentPng from "../constants/transparentPng";
+
+/**
+ * Triggered when there was an error loading a character sprite.
+ * @param {HTMLImageElement} image the element containing the missing image
+ */
+export function charError(image: HTMLImageElement) {
+ console.warn(`${image.src} is missing from webAO`);
+ image.src = transparentPng;
+ return true;
+}
+window.charError = charError; \ No newline at end of file
diff --git a/webAO/dom/charTableFilter.ts b/webAO/dom/charTableFilter.ts
new file mode 100644
index 0000000..d81fb88
--- /dev/null
+++ b/webAO/dom/charTableFilter.ts
@@ -0,0 +1,20 @@
+import { client } from '../client'
+/**
+ * Triggered when the music search bar is changed
+ * @param {MouseEvent} event
+ */
+export function chartable_filter(_event: Event) {
+ const searchname = (<HTMLInputElement>(
+ document.getElementById("client_charactersearch")
+ )).value;
+
+ 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";
+ } else {
+ demothing.style.display = "inline-block";
+ }
+ });
+}
+window.chartable_filter = chartable_filter; \ No newline at end of file
diff --git a/webAO/dom/deleteEvidence.ts b/webAO/dom/deleteEvidence.ts
new file mode 100644
index 0000000..cd299e4
--- /dev/null
+++ b/webAO/dom/deleteEvidence.ts
@@ -0,0 +1,12 @@
+import { client } from "../client";
+import { cancelEvidence } from "./cancelEvidence";
+
+/**
+ * Delete selected evidence.
+ */
+export function deleteEvidence() {
+ const id = client.selectedEvidence - 1;
+ client.sender.sendDE(id);
+ cancelEvidence();
+}
+window.deleteEvidence = deleteEvidence; \ No newline at end of file
diff --git a/webAO/dom/editEvidence.ts b/webAO/dom/editEvidence.ts
new file mode 100644
index 0000000..931caad
--- /dev/null
+++ b/webAO/dom/editEvidence.ts
@@ -0,0 +1,22 @@
+import { client } from '../client'
+import { cancelEvidence } from './cancelEvidence';
+
+/**
+ * Edit selected evidence.
+ */
+export function editEvidence() {
+ const evidence_select = <HTMLSelectElement>(
+ document.getElementById("evi_select")
+ );
+ const id = client.selectedEvidence - 1;
+ client.sender.sendEE(
+ id,
+ (<HTMLInputElement>document.getElementById("evi_name")).value,
+ (<HTMLInputElement>document.getElementById("evi_desc")).value,
+ evidence_select.selectedIndex === 0
+ ? (<HTMLInputElement>document.getElementById("evi_filename")).value
+ : evidence_select.options[evidence_select.selectedIndex].text
+ );
+ cancelEvidence();
+}
+window.editEvidence = editEvidence; \ No newline at end of file
diff --git a/webAO/dom/getIndexFromSelect.ts b/webAO/dom/getIndexFromSelect.ts
new file mode 100644
index 0000000..2f21653
--- /dev/null
+++ b/webAO/dom/getIndexFromSelect.ts
@@ -0,0 +1,16 @@
+/**
+ * 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 = <HTMLSelectElement>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; \ No newline at end of file
diff --git a/webAO/dom/guilty.ts b/webAO/dom/guilty.ts
new file mode 100644
index 0000000..f008065
--- /dev/null
+++ b/webAO/dom/guilty.ts
@@ -0,0 +1,9 @@
+import { client } from "../client";
+
+/**
+ * Declare the defendant not guilty
+ */
+export function guilty() {
+ client.sender.sendRT("judgeruling#1");
+}
+window.guilty = guilty; \ No newline at end of file
diff --git a/webAO/dom/imgError.ts b/webAO/dom/imgError.ts
new file mode 100644
index 0000000..fdb6122
--- /dev/null
+++ b/webAO/dom/imgError.ts
@@ -0,0 +1,10 @@
+/**
+ * Triggered when there was an error loading a generic sprite.
+ * @param {HTMLImageElement} image the element containing the missing image
+ */
+export function imgError(image: HTMLImageElement) {
+ image.onerror = null;
+ image.src = ""; // unload so the old sprite doesn't persist
+ return true;
+}
+window.imgError = imgError; \ No newline at end of file
diff --git a/webAO/dom/iniEdit.ts b/webAO/dom/iniEdit.ts
new file mode 100644
index 0000000..0710de9
--- /dev/null
+++ b/webAO/dom/iniEdit.ts
@@ -0,0 +1,15 @@
+import { client } from "../client";
+import { handleCharacterInfo } from "../client/handleCharacterInfo";
+import { packetHandler } from "../packets/packetHandler";
+
+/**
+ * Triggered by the ini button.
+ */
+export async function iniedit() {
+ const ininame = (<HTMLInputElement>document.getElementById("client_ininame"))
+ .value;
+ const inicharID = client.charID;
+ await handleCharacterInfo(ininame.split("&"), inicharID);
+ packetHandler.get("PV")!(`PV#0#CID#${inicharID}`.split("#"));
+}
+window.iniedit = iniedit;
diff --git a/webAO/dom/initCE.ts b/webAO/dom/initCE.ts
new file mode 100644
index 0000000..fd57d74
--- /dev/null
+++ b/webAO/dom/initCE.ts
@@ -0,0 +1,9 @@
+import { client } from "../client";
+
+/**
+ * Declare cross examination.
+ */
+export function initCE() {
+ client.sender.sendRT("testimony2");
+}
+window.initCE = initCE; \ No newline at end of file
diff --git a/webAO/dom/initWT.ts b/webAO/dom/initWT.ts
new file mode 100644
index 0000000..d99fc3e
--- /dev/null
+++ b/webAO/dom/initWT.ts
@@ -0,0 +1,9 @@
+import { client } from "../client";
+
+/**
+ * Declare witness testimony.
+ */
+export function initWT() {
+ client.sender.sendRT("testimony1");
+}
+window.initWT = initWT; \ No newline at end of file
diff --git a/webAO/dom/modCallTest.ts b/webAO/dom/modCallTest.ts
new file mode 100644
index 0000000..7058caf
--- /dev/null
+++ b/webAO/dom/modCallTest.ts
@@ -0,0 +1,8 @@
+import { packetHandler } from "../packets/packetHandler";
+/**
+ * Triggered by the modcall sfx dropdown
+ */
+export function modcall_test() {
+ packetHandler.get("ZZ")!("test#test".split("#"));
+}
+window.modcall_test = modcall_test; \ No newline at end of file
diff --git a/webAO/dom/musicListClick.ts b/webAO/dom/musicListClick.ts
new file mode 100644
index 0000000..2243553
--- /dev/null
+++ b/webAO/dom/musicListClick.ts
@@ -0,0 +1,21 @@
+import { client } from "../client";
+/**
+ * Triggered when an item on the music list is clicked.
+ * @param {MouseEvent} event
+ */
+export function musiclist_click(_event: Event) {
+ const playtrack = (<HTMLInputElement>(
+ document.getElementById("client_musiclist")
+ )).value;
+ client.sender.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 = (<HTMLSelectElement>(
+ document.getElementById("client_musiclist")
+ )).selectedOptions;
+ for (let i = 0; i < musiclist_elements.length; i++) {
+ musiclist_elements[i].selected = false;
+ }
+}
+window.musiclist_click = musiclist_click;
diff --git a/webAO/dom/musicListFilter.ts b/webAO/dom/musicListFilter.ts
new file mode 100644
index 0000000..3db7fcd
--- /dev/null
+++ b/webAO/dom/musicListFilter.ts
@@ -0,0 +1,24 @@
+import { client } from '../client'
+/**
+ * Triggered when the music search bar is changed
+ * @param {MouseEvent} event
+ */
+export function musiclist_filter(_event: Event) {
+ const musiclist_element = <HTMLSelectElement>(
+ document.getElementById("client_musiclist")
+ );
+ const searchname = (<HTMLInputElement>(
+ document.getElementById("client_musicsearch")
+ )).value;
+
+ musiclist_element.innerHTML = "";
+
+ for (const trackname of client.musics) {
+ if (trackname.toLowerCase().indexOf(searchname.toLowerCase()) !== -1) {
+ const newentry = <HTMLOptionElement>document.createElement("OPTION");
+ newentry.text = trackname;
+ musiclist_element.options.add(newentry);
+ }
+ }
+}
+window.musiclist_filter = musiclist_filter; \ No newline at end of file
diff --git a/webAO/dom/notGuilty.ts b/webAO/dom/notGuilty.ts
new file mode 100644
index 0000000..95b830b
--- /dev/null
+++ b/webAO/dom/notGuilty.ts
@@ -0,0 +1,9 @@
+import { client } from "../client";
+
+/**
+ * Declare the defendant not guilty
+ */
+export function notguilty() {
+ client.sender.sendRT("judgeruling#0");
+}
+window.notguilty = notguilty; \ No newline at end of file
diff --git a/webAO/dom/onEnter.ts b/webAO/dom/onEnter.ts
new file mode 100644
index 0000000..5ab532c
--- /dev/null
+++ b/webAO/dom/onEnter.ts
@@ -0,0 +1,103 @@
+import { client, selectedShout } from "../client";
+import { escapeChat } from "../encoding";
+
+
+/**
+ * Triggered when the Return key is pressed on the in-character chat input box.
+ * @param {KeyboardEvent} event
+ */
+ export function onEnter(event: KeyboardEvent) {
+ if (event.keyCode === 13) {
+ const mychar = client.character;
+ const myemo = client.emote;
+ const evi = client.evidence;
+ 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(
+ (<HTMLInputElement>document.getElementById("check_nonint")).checked
+ );
+ const looping_sfx = Boolean(
+ (<HTMLInputElement>document.getElementById("check_loopsfx")).checked
+ );
+ const color = Number(
+ (<HTMLInputElement>document.getElementById("textcolor")).value
+ );
+ const showname = escapeChat(
+ (<HTMLInputElement>document.getElementById("ic_chat_name")).value
+ );
+ const text = (<HTMLInputElement>document.getElementById("client_inputbox"))
+ .value;
+ const pairchar = (<HTMLInputElement>document.getElementById("pair_select"))
+ .value;
+ const pairoffset = Number(
+ (<HTMLInputElement>document.getElementById("pair_offset")).value
+ );
+ const pairyoffset = Number(
+ (<HTMLInputElement>document.getElementById("pair_y_offset")).value
+ );
+ const myrole = (<HTMLInputElement>document.getElementById("role_select"))
+ .value
+ ? (<HTMLInputElement>document.getElementById("role_select")).value
+ : mychar.side;
+ const additive = Boolean(
+ (<HTMLInputElement>document.getElementById("check_additive")).checked
+ );
+ const effect = (<HTMLInputElement>document.getElementById("effect_select"))
+ .value;
+
+ let sfxname = "0";
+ let sfxdelay = 0;
+ let emote_mod = myemo.zoom;
+ if ((<HTMLInputElement>document.getElementById("sendsfx")).checked) {
+ sfxname = myemo.sfx;
+ sfxdelay = myemo.sfxdelay;
+ }
+
+ // not to overwrite a 5 from the ini or anything else
+ if ((<HTMLInputElement>document.getElementById("sendpreanim")).checked) {
+ if (emote_mod === 0) {
+ emote_mod = 1;
+ }
+ } else if (emote_mod === 1) {
+ emote_mod = 0;
+ }
+
+ client.sender.sendIC(
+ myemo.deskmod,
+ 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
+ );
+ }
+ return false;
+ }
+ window.onEnter = onEnter; \ No newline at end of file
diff --git a/webAO/dom/onOOCEnter.ts b/webAO/dom/onOOCEnter.ts
new file mode 100644
index 0000000..d7ec21b
--- /dev/null
+++ b/webAO/dom/onOOCEnter.ts
@@ -0,0 +1,15 @@
+import { client } from "../client";
+/**
+ * Triggered when the Return key is pressed on the out-of-character chat input box.
+ * @param {KeyboardEvent} event
+ */
+export function onOOCEnter(event: KeyboardEvent) {
+ if (event.keyCode === 13) {
+ client.sender.sendOOC(
+ (<HTMLInputElement>document.getElementById("client_oocinputbox")).value
+ );
+ (<HTMLInputElement>document.getElementById("client_oocinputbox")).value =
+ "";
+ }
+}
+window.onOOCEnter = onOOCEnter;
diff --git a/webAO/dom/onReplayGo.ts b/webAO/dom/onReplayGo.ts
new file mode 100644
index 0000000..82a6f2f
--- /dev/null
+++ b/webAO/dom/onReplayGo.ts
@@ -0,0 +1,10 @@
+import { client } from "../client";
+
+/**
+ * Triggered when the user click replay GOOOOO
+ * @param {KeyboardEvent} event
+ */
+export function onReplayGo(_event: Event) {
+ client.handleReplay();
+}
+window.onReplayGo = onReplayGo; \ No newline at end of file
diff --git a/webAO/dom/opusCheck.ts b/webAO/dom/opusCheck.ts
new file mode 100644
index 0000000..939fdc6
--- /dev/null
+++ b/webAO/dom/opusCheck.ts
@@ -0,0 +1,22 @@
+/**
+ * Triggered when there was an error loading a sound
+ * @param {HTMLAudioElement} image the element containing the missing sound
+ */
+export function opusCheck(
+ channel: HTMLAudioElement
+): OnErrorEventHandlerNonNull {
+ 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; \ No newline at end of file
diff --git a/webAO/dom/pickChar.ts b/webAO/dom/pickChar.ts
new file mode 100644
index 0000000..82fb6af
--- /dev/null
+++ b/webAO/dom/pickChar.ts
@@ -0,0 +1,16 @@
+import { client } from "../client";
+
+/**
+ * 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: number) {
+ if (ccharacter === -1) {
+ // Spectator
+ document.getElementById("client_waiting")!.style.display = "none";
+ document.getElementById("client_charselect")!.style.display = "none";
+ }
+ client.sender.sendCharacter(ccharacter);
+}
+window.pickChar = pickChar; \ No newline at end of file
diff --git a/webAO/dom/pickEmotion.ts b/webAO/dom/pickEmotion.ts
new file mode 100644
index 0000000..b72583f
--- /dev/null
+++ b/webAO/dom/pickEmotion.ts
@@ -0,0 +1,24 @@
+import { client } from '../client'
+/**
+ * Highlights and selects an emotion for in-character chat.
+ * @param {string} emo the new emotion to be selected
+ */
+export function pickEmotion(emo: number) {
+ 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";
+
+ (<HTMLInputElement>document.getElementById("sendsfx")).checked =
+ client.emote.sfx.length > 1;
+
+ (<HTMLInputElement>document.getElementById("sendpreanim")).checked =
+ client.emote.zoom == 1;
+}
+window.pickEmotion = pickEmotion; \ No newline at end of file
diff --git a/webAO/dom/pickEvidence.ts b/webAO/dom/pickEvidence.ts
new file mode 100644
index 0000000..411acc1
--- /dev/null
+++ b/webAO/dom/pickEvidence.ts
@@ -0,0 +1,51 @@
+import { client } from '../client'
+import { cancelEvidence } from './cancelEvidence';
+import { updateEvidenceIcon } from './updateEvidenceIcon'
+import { getIndexFromSelect } from './getIndexFromSelect'
+
+/**
+ * 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
+ (<HTMLInputElement>document.getElementById("evi_name")).value =
+ client.evidences[evidence - 1].name;
+ (<HTMLInputElement>document.getElementById("evi_desc")).value =
+ client.evidences[evidence - 1].desc;
+
+ // Update icon
+ const icon_id = getIndexFromSelect(
+ "evi_select",
+ client.evidences[evidence - 1].filename
+ );
+ (<HTMLSelectElement>document.getElementById("evi_select")).selectedIndex =
+ icon_id;
+ if (icon_id === 0) {
+ (<HTMLInputElement>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;
diff --git a/webAO/dom/randomCharacterOOC.ts b/webAO/dom/randomCharacterOOC.ts
new file mode 100644
index 0000000..657a474
--- /dev/null
+++ b/webAO/dom/randomCharacterOOC.ts
@@ -0,0 +1,8 @@
+import { client } from '../client'
+/**
+ * Random character via OOC.
+ */
+export function randomCharacterOOC() {
+ client.sender.sendOOC(`/randomchar`);
+}
+window.randomCharacterOOC = randomCharacterOOC; \ No newline at end of file
diff --git a/webAO/dom/reconnectButton.ts b/webAO/dom/reconnectButton.ts
new file mode 100644
index 0000000..4031ccd
--- /dev/null
+++ b/webAO/dom/reconnectButton.ts
@@ -0,0 +1,16 @@
+import Client, { client, setClient } from "../client";
+import queryParser from "../utils/queryParser";
+let { ip: serverIP } = queryParser();
+
+/**
+ * Triggered when the reconnect button is pushed.
+ */
+export function ReconnectButton() {
+ client.cleanup();
+ setClient(new Client(serverIP));
+
+ if (client) {
+ document.getElementById("client_error")!.style.display = "none";
+ }
+}
+window.ReconnectButton = ReconnectButton; \ No newline at end of file
diff --git a/webAO/dom/redHPD.ts b/webAO/dom/redHPD.ts
new file mode 100644
index 0000000..e228d21
--- /dev/null
+++ b/webAO/dom/redHPD.ts
@@ -0,0 +1,9 @@
+import { client } from "../client";
+
+/**
+ * Decrement defense health point.
+ */
+export function redHPD() {
+ client.sender.sendHP(1, client.hp[0] - 1);
+}
+window.redHPD = redHPD; \ No newline at end of file
diff --git a/webAO/dom/redHPP.ts b/webAO/dom/redHPP.ts
new file mode 100644
index 0000000..efde941
--- /dev/null
+++ b/webAO/dom/redHPP.ts
@@ -0,0 +1,9 @@
+import { client } from "../client";
+
+/**
+ * Decrement prosecution health point.
+ */
+export function redHPP() {
+ client.sender.sendHP(2, client.hp[1] - 1);
+}
+window.redHPP = redHPP; \ No newline at end of file
diff --git a/webAO/dom/reloadTheme.ts b/webAO/dom/reloadTheme.ts
new file mode 100644
index 0000000..bfa46b6
--- /dev/null
+++ b/webAO/dom/reloadTheme.ts
@@ -0,0 +1,16 @@
+import { client } from '../client'
+import setCookie from '../utils/setCookie';
+
+/**
+ * Triggered by the theme selector.
+ */
+export const reloadTheme = () => {
+ client.viewport.setTheme((<HTMLSelectElement>document.getElementById("client_themeselect"))
+ .value);
+
+ setCookie("theme", client.viewport.getTheme());
+ (<HTMLAnchorElement>(
+ document.getElementById("client_theme")
+ )).href = `styles/${client.viewport.getTheme()}.css`;
+}
+window.reloadTheme = reloadTheme; \ No newline at end of file
diff --git a/webAO/dom/resetOffset.ts b/webAO/dom/resetOffset.ts
new file mode 100644
index 0000000..86dfd5b
--- /dev/null
+++ b/webAO/dom/resetOffset.ts
@@ -0,0 +1,6 @@
+
+export function resetOffset(_event: Event) {
+ (<HTMLInputElement>document.getElementById("pair_offset")).value = "0";
+ (<HTMLInputElement>document.getElementById("pair_y_offset")).value = "0";
+}
+window.resetOffset = resetOffset; \ No newline at end of file
diff --git a/webAO/dom/resizeChatbox.ts b/webAO/dom/resizeChatbox.ts
new file mode 100644
index 0000000..efb8bdc
--- /dev/null
+++ b/webAO/dom/resizeChatbox.ts
@@ -0,0 +1,33 @@
+import { CHATBOX } from "../client";
+/**
+ * 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`;
+
+ const trackstatus = <HTMLMarqueeElement>(document.getElementById("client_trackstatustext"));
+ trackstatus.width = (trackstatus.offsetWidth - 1) + "px";
+
+
+ //clock
+ const now = new Date();
+ let weekday = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
+ const month = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
+ document.getElementById("client_clock_month")!.innerText = month[now.getMonth()];
+ console.debug(CHATBOX);
+ if (CHATBOX == "acww") {
+ weekday = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
+ document.getElementById("client_clock_weekday")!.innerText = weekday[now.getDay()];
+ document.getElementById("client_clock_date")!.innerText = now.getDay() + "/" + now.getMonth();
+ document.getElementById("client_clock_time")!.innerText = now.getHours() + ":" + now.getMinutes();
+ } else if (CHATBOX == "key") {
+ weekday = ["Sun.", "Mon.", "Tue.", "Wed.", "Thu.", "Fri.", "Sat."];
+ document.getElementById("client_clock_weekday")!.innerText = weekday[now.getDay()];
+ document.getElementById("client_clock_date")!.innerText = String(now.getDay());
+ }
+
+}
+window.resizeChatbox = resizeChatbox; \ No newline at end of file
diff --git a/webAO/dom/setChatbox.ts b/webAO/dom/setChatbox.ts
new file mode 100644
index 0000000..6d1a78c
--- /dev/null
+++ b/webAO/dom/setChatbox.ts
@@ -0,0 +1,28 @@
+import { CHATBOX, setCHATBOX } from "../client";
+import chatbox_arr from "../styles/chatbox/chatboxes.js";
+import setCookie from "../utils/setCookie";
+
+/**
+ * Set the style of the chatbox
+ */
+export function setChatbox(style: string) {
+ const chatbox_theme = <HTMLAnchorElement>(
+ document.getElementById("chatbox_theme")
+ );
+ const themeselect = <HTMLSelectElement>(
+ document.getElementById("client_chatboxselect")
+ );
+ setCHATBOX(themeselect.value);
+
+ setCookie("chatbox", CHATBOX);
+ if (CHATBOX === "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/${CHATBOX}.css`;
+ }
+}
+window.setChatbox = setChatbox; \ No newline at end of file
diff --git a/webAO/dom/showNameClick.ts b/webAO/dom/showNameClick.ts
new file mode 100644
index 0000000..3e48b70
--- /dev/null
+++ b/webAO/dom/showNameClick.ts
@@ -0,0 +1,26 @@
+import setCookie from "../utils/setCookie";
+
+
+/**
+ * Triggered when the showname checkboc is clicked
+ * @param {MouseEvent} event
+ */
+export function showname_click(_event: Event | null) {
+ setCookie(
+ "showname",
+ String((<HTMLInputElement>document.getElementById("showname")).checked)
+ );
+ setCookie(
+ "ic_chat_name",
+ (<HTMLInputElement>document.getElementById("ic_chat_name")).value
+ );
+
+ const css_s = <HTMLAnchorElement>document.getElementById("nameplate_setting");
+
+ if ((<HTMLInputElement>document.getElementById("showname")).checked) {
+ css_s.href = "styles/shownames.css";
+ } else {
+ css_s.href = "styles/nameplates.css";
+ }
+}
+window.showname_click = showname_click;
diff --git a/webAO/dom/switchAspectRatio.ts b/webAO/dom/switchAspectRatio.ts
new file mode 100644
index 0000000..79d4110
--- /dev/null
+++ b/webAO/dom/switchAspectRatio.ts
@@ -0,0 +1,19 @@
+/**
+ * Triggered by the change aspect ratio checkbox
+ */
+export async function switchAspectRatio() {
+ const background = document.getElementById("client_gamewindow")!;
+ const offsetCheck = <HTMLInputElement>(
+ document.getElementById("client_hdviewport_offset")
+ );
+ if (
+ (<HTMLInputElement>document.getElementById("client_hdviewport")).checked
+ ) {
+ background.style.paddingBottom = "56.25%";
+ offsetCheck.disabled = false;
+ } else {
+ background.style.paddingBottom = "75%";
+ offsetCheck.disabled = true;
+ }
+}
+window.switchAspectRatio = switchAspectRatio; \ No newline at end of file
diff --git a/webAO/dom/switchChatOffset.ts b/webAO/dom/switchChatOffset.ts
new file mode 100644
index 0000000..6552cbd
--- /dev/null
+++ b/webAO/dom/switchChatOffset.ts
@@ -0,0 +1,17 @@
+/**
+ * Triggered by the change aspect ratio checkbox
+ */
+export async function switchChatOffset() {
+ const container = document.getElementById("client_chatcontainer")!;
+ if (
+ (<HTMLInputElement>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; \ No newline at end of file
diff --git a/webAO/dom/switchPanTilt.ts b/webAO/dom/switchPanTilt.ts
new file mode 100644
index 0000000..7ceea06
--- /dev/null
+++ b/webAO/dom/switchPanTilt.ts
@@ -0,0 +1,16 @@
+/**
+ * Triggered by the pantilt checkbox
+ */
+export async function switchPanTilt() {
+ const fullview = document.getElementById("client_fullview")!;
+ const checkbox = <HTMLInputElement>document.getElementById("client_pantilt");
+
+ if (checkbox.checked) {
+ fullview.style.transition = "0.5s ease-in-out";
+ } else {
+ fullview.style.transition = "none";
+ }
+
+ return;
+}
+window.switchPanTilt = switchPanTilt; \ No newline at end of file
diff --git a/webAO/dom/toggleElement.js b/webAO/dom/toggleElement.js
new file mode 100644
index 0000000..efddabf
--- /dev/null
+++ b/webAO/dom/toggleElement.js
@@ -0,0 +1,13 @@
+/**
+ * Hides and shows any html element
+ * @param {string} element_id the id of the element to toggle
+ */
+export function toggleElement(element_id) {
+ const element = document.getElementById(element_id);
+ if (element.style.display !== 'none') {
+ element.style.display = 'none';
+ } else {
+ element.style.display = 'block';
+ }
+}
+window.toggleElement = toggleElement;
diff --git a/webAO/dom/toggleMenu.ts b/webAO/dom/toggleMenu.ts
new file mode 100644
index 0000000..6d5e1fc
--- /dev/null
+++ b/webAO/dom/toggleMenu.ts
@@ -0,0 +1,18 @@
+import { selectedMenu, setSelectedMenu } from "../client";
+
+/**
+ * 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";
+ setSelectedMenu(menu);
+ }
+}
+window.toggleMenu = toggleMenu;
diff --git a/webAO/dom/toggleShout.ts b/webAO/dom/toggleShout.ts
new file mode 100644
index 0000000..cb12f49
--- /dev/null
+++ b/webAO/dom/toggleShout.ts
@@ -0,0 +1,21 @@
+import { selectedShout, setSelectedShout } from "../client";
+
+/**
+ * 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";
+ setSelectedShout(0);
+ } else {
+ document.getElementById(`button_${shout}`)!.className = "client_button dark";
+ if (selectedShout) {
+ document.getElementById(`button_${selectedShout}`)!.className =
+ "client_button";
+ }
+ setSelectedShout(shout);
+ }
+}
+window.toggleShout = toggleShout;
diff --git a/webAO/dom/updateActionCommands.ts b/webAO/dom/updateActionCommands.ts
new file mode 100644
index 0000000..9d0bd82
--- /dev/null
+++ b/webAO/dom/updateActionCommands.ts
@@ -0,0 +1,27 @@
+
+/**
+ * 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 = <HTMLSelectElement>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; \ No newline at end of file
diff --git a/webAO/dom/updateBackgroundPreview.ts b/webAO/dom/updateBackgroundPreview.ts
new file mode 100644
index 0000000..b41ee8f
--- /dev/null
+++ b/webAO/dom/updateBackgroundPreview.ts
@@ -0,0 +1,28 @@
+import { AO_HOST } from '../client/aoHost'
+/**
+ * Update background preview.
+ */
+export function updateBackgroundPreview() {
+ const background_select = <HTMLSelectElement>(
+ document.getElementById("bg_select")
+ );
+ const background_filename = <HTMLInputElement>(
+ document.getElementById("bg_filename")
+ );
+ const background_preview = <HTMLImageElement>(
+ 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; \ No newline at end of file
diff --git a/webAO/dom/updateEvidenceIcon.ts b/webAO/dom/updateEvidenceIcon.ts
new file mode 100644
index 0000000..bff0475
--- /dev/null
+++ b/webAO/dom/updateEvidenceIcon.ts
@@ -0,0 +1,28 @@
+import { AO_HOST } from "../client/aoHost";
+/**
+ * Update evidence icon.
+ */
+export function updateEvidenceIcon() {
+ const evidence_select = <HTMLSelectElement>(
+ document.getElementById("evi_select")
+ );
+ const evidence_filename = <HTMLInputElement>(
+ document.getElementById("evi_filename")
+ );
+ const evidence_iconbox = <HTMLImageElement>(
+ 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;
diff --git a/webAO/dom/window.ts b/webAO/dom/window.ts
new file mode 100644
index 0000000..2535768
--- /dev/null
+++ b/webAO/dom/window.ts
@@ -0,0 +1,56 @@
+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: HTMLAudioElement) => OnErrorEventHandlerNonNull;
+ 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;
+ }
+}
+export { } \ No newline at end of file
diff --git a/webAO/encoding.ts b/webAO/encoding.ts
index 1018144..54770d0 100644
--- a/webAO/encoding.ts
+++ b/webAO/encoding.ts
@@ -38,14 +38,6 @@ export function safeTags(unsafe: string): string {
}
/**
- * Encode text on client side.
- * @param {string} estring the string to be encoded
- */
-export function encodeChat(estring: string): string {
- return estring;
-}
-
-/**
* Decodes text on client side.
* @param {string} estring the string to be decoded
*/
diff --git a/webAO/index.ts b/webAO/index.ts
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/webAO/index.ts
diff --git a/webAO/master.ts b/webAO/master.ts
index 5c538d4..7e9c994 100644
--- a/webAO/master.ts
+++ b/webAO/master.ts
@@ -20,10 +20,10 @@ let selectedServer: number = -1;
let servers: { name: string, description: string, ip: string, port: number, ws_port: number, assets: string, online: string }[] = [];
servers[-2] = {
- name: 'Singleplayer', description: 'Build cases, try out new things', ip: '127.0.0.1', port: 50001, ws_port: 50001, assets: '', online: '',
+ name: 'Singleplayer', description: 'Build cases, try out new things', ip: '127.0.0.1', port: 50001, ws_port: 50001, assets: '', online: 'Online: 0/1',
};
servers[-1] = {
- name: 'Localhost', description: 'This is your computer on port 50001', ip: '127.0.0.1', port: 50001, ws_port: 50001, assets: '', online: 'Online: ?/?',
+ name: 'Localhost', description: 'This is your computer on port 50001', ip: '127.0.0.1', port: 50001, ws_port: 50001, assets: '', online: 'Offline',
};
const fpPromise = FingerprintJS.load();
@@ -58,11 +58,7 @@ export function setServ(ID: number) {
if (document.getElementById(`server${ID}`).className === '') { checkOnline(ID, `${servers[ID].ip}:${servers[ID].ws_port}`); }
- if (servers[ID].description !== undefined) {
- document.getElementById('serverdescription_content').innerHTML = `<b>${servers[ID].online}</b><br>${safeTags(servers[ID].description)}`;
- } else {
- document.getElementById('serverdescription_content').innerHTML = '';
- }
+ document.getElementById('serverdescription_content').innerHTML = `<b>${servers[ID].online}</b><br>${safeTags(servers[ID].description)}`;
}
window.setServ = setServ;
@@ -132,11 +128,12 @@ 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://');
+ const myURL: string = window.location.href.replace('https://','http://').replace('index.html','');
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];
servers[i] = serverEntry;
+ servers[i].online = "Offline";
const ipport = `${serverEntry.ip}:${serverEntry.ws_port}`;
diff --git a/webAO/packets/handlers/handleARUP.ts b/webAO/packets/handlers/handleARUP.ts
new file mode 100644
index 0000000..97db9cc
--- /dev/null
+++ b/webAO/packets/handlers/handleARUP.ts
@@ -0,0 +1,42 @@
+import { client } from "../../client";
+import { safeTags } from "../../encoding";
+
+/**
+ * Handle the change of players in an area.
+ * @param {Array} args packet arguments
+ */
+export const handleARUP = (args: string[]) => {
+ args = args.slice(1);
+ for (let i = 0; i < args.length - 2; i++) {
+ if (client.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
+ client.areas[i].players = Number(args[i + 1]);
+ break;
+ case 1: // status
+ client.areas[i].status = safeTags(args[i + 1]);
+ break;
+ case 2:
+ client.areas[i].cm = safeTags(args[i + 1]);
+ break;
+ case 3:
+ client.areas[i].locked = safeTags(args[i + 1]);
+ break;
+ }
+
+ thisarea.className = `area-button area-${client.areas[
+ i
+ ].status.toLowerCase()}`;
+
+ thisarea.innerText = `${client.areas[i].name} (${client.areas[i].players}) [${client.areas[i].status}]`;
+
+ thisarea.title =
+ `Players: ${client.areas[i].players}\n` +
+ `Status: ${client.areas[i].status}\n` +
+ `CM: ${client.areas[i].cm}\n` +
+ `Area lock: ${client.areas[i].locked}`;
+ }
+ }
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleASS.ts b/webAO/packets/handlers/handleASS.ts
new file mode 100644
index 0000000..ea8a0d3
--- /dev/null
+++ b/webAO/packets/handlers/handleASS.ts
@@ -0,0 +1,10 @@
+import { setAOhost } from "../../client/aoHost";
+
+
+/**
+* new asset url!!
+* @param {Array} args packet arguments
+*/
+export const handleASS = (args: string[]) => {
+ setAOhost(args[1]);
+}
diff --git a/webAO/packets/handlers/handleBB.ts b/webAO/packets/handlers/handleBB.ts
new file mode 100644
index 0000000..c12c4f6
--- /dev/null
+++ b/webAO/packets/handlers/handleBB.ts
@@ -0,0 +1,11 @@
+import { safeTags } from "../../encoding";
+
+
+/**
+ * Handles the warning packet
+ * on client this spawns a message box you can't close for 2 seconds
+ * @param {Array} args ban reason
+ */
+export const handleBB = (args: string[]) => {
+ alert(safeTags(args[1]));
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleBD.ts b/webAO/packets/handlers/handleBD.ts
new file mode 100644
index 0000000..dbfb54b
--- /dev/null
+++ b/webAO/packets/handlers/handleBD.ts
@@ -0,0 +1,14 @@
+import { setBanned } from "../../client";
+import { safeTags } from "../../encoding";
+import { handleBans } from '../../client/handleBans'
+
+
+/**
+ * Handles the banned packet
+ * this one is sent when you try to reconnect but you're banned
+ * @param {Array} args ban reason
+ */
+export const handleBD = (args: string[]) => {
+ handleBans("Banned", safeTags(args[1]));
+ setBanned(true);
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleBN.ts b/webAO/packets/handlers/handleBN.ts
new file mode 100644
index 0000000..3c5d95f
--- /dev/null
+++ b/webAO/packets/handlers/handleBN.ts
@@ -0,0 +1,84 @@
+import { client } from "../../client";
+import { AO_HOST } from "../../client/aoHost";
+import { safeTags } from "../../encoding";
+import { updateBackgroundPreview } from '../../dom/updateBackgroundPreview'
+import { getIndexFromSelect } from '../../dom/getIndexFromSelect'
+import tryUrls from "../../utils/tryUrls";
+
+
+/**
+ * Handles a background change.
+ * @param {Array} args packet arguments
+ */
+
+export const handleBN = (args: string[]) => {
+ const bgFromArgs = safeTags(args[1]);
+ client.viewport.setBackgroundName(bgFromArgs);
+ const bgfolder = client.viewport.getBackgroundFolder();
+ const bg_index = getIndexFromSelect(
+ "bg_select",
+ client.viewport.getBackgroundName()
+ );
+ (<HTMLSelectElement>document.getElementById("bg_select")).selectedIndex =
+ bg_index;
+ updateBackgroundPreview();
+ if (bg_index === 0) {
+ (<HTMLInputElement>document.getElementById("bg_filename")).value =
+ client.viewport.getBackgroundName();
+ }
+
+ tryUrls(
+ `${AO_HOST}background/${encodeURI(args[1].toLowerCase())}/defenseempty`
+ ).then((resp) => {
+ (<HTMLImageElement>document.getElementById("bg_preview")).src = resp;
+ });
+ tryUrls(`${bgfolder}defensedesk`).then((resp) => {
+ (<HTMLImageElement>document.getElementById("client_def_bench")).src =
+ resp;
+ });
+ tryUrls(`${bgfolder}stand`).then((resp) => {
+ (<HTMLImageElement>document.getElementById("client_wit_bench")).src =
+ resp;
+ });
+ tryUrls(`${bgfolder}prosecutiondesk`).then((resp) => {
+ (<HTMLImageElement>document.getElementById("client_pro_bench")).src =
+ resp;
+ });
+ tryUrls(`${bgfolder}full`).then((resp) => {
+ (<HTMLImageElement>document.getElementById("client_court")).src = resp;
+ });
+ tryUrls(`${bgfolder}defenseempty`).then((resp) => {
+ (<HTMLImageElement>document.getElementById("client_court_def")).src =
+ resp;
+ });
+ tryUrls(`${bgfolder}transition_def`).then((resp) => {
+ (<HTMLImageElement>document.getElementById("client_court_deft")).src =
+ resp;
+ });
+ tryUrls(`${bgfolder}witnessempty`).then((resp) => {
+ (<HTMLImageElement>document.getElementById("client_court_wit")).src =
+ resp;
+ });
+ tryUrls(`${bgfolder}transition_pro`).then((resp) => {
+ (<HTMLImageElement>document.getElementById("client_court_prot")).src =
+ resp;
+ });
+ tryUrls(`${bgfolder}prosecutorempty`).then((resp) => {
+ (<HTMLImageElement>document.getElementById("client_court_pro")).src =
+ resp;
+ });
+
+ if (client.charID === -1) {
+ client.viewport.set_side({
+ position: "jud",
+ showSpeedLines: false,
+ showDesk: true,
+ });
+ } else {
+ client.viewport.set_side({
+ position: client.chars[client.charID].side,
+ showSpeedLines: false,
+ showDesk: true,
+ });
+ }
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleCC.ts b/webAO/packets/handlers/handleCC.ts
new file mode 100644
index 0000000..36bcdc7
--- /dev/null
+++ b/webAO/packets/handlers/handleCC.ts
@@ -0,0 +1,9 @@
+import { client } from "../../client";
+
+/**
+ * What? you want a character??
+ * @param {Array} args packet arguments
+ */
+export const handleCC = (args: string[]) => {
+ client.sender.sendSelf(`PV#1#CID#${args[2]}#%`);
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleCI.ts b/webAO/packets/handlers/handleCI.ts
new file mode 100644
index 0000000..cb693bc
--- /dev/null
+++ b/webAO/packets/handlers/handleCI.ts
@@ -0,0 +1,27 @@
+import { client } from '../../client'
+import { handleCharacterInfo } from '../../client/handleCharacterInfo'
+/**
+ * Handles incoming character information, bundling multiple characters
+ * per packet.
+ * CI#0#Phoenix&description&&&&&#1#Miles ...
+ * @param {Array} args packet arguments
+ */
+export const handleCI = (args: string[]) => {
+ // 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]}/${client.char_list_length}`;
+ const chargs = args[i].split("&");
+ const charid = Number(args[i - 1]);
+ (<HTMLProgressElement>(
+ document.getElementById("client_loadingbar")
+ )).value = charid;
+ setTimeout(() => handleCharacterInfo(chargs, charid), 500);
+ }
+ }
+ // Request the next pack
+ client.sender.sendServer(`AN#${Number(args[1]) / 10 + 1}#%`);
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleCT.ts b/webAO/packets/handlers/handleCT.ts
new file mode 100644
index 0000000..cff9b24
--- /dev/null
+++ b/webAO/packets/handlers/handleCT.ts
@@ -0,0 +1,17 @@
+import queryParser from '../../utils/queryParser'
+import { prepChat } from '../../encoding'
+let { mode } = queryParser();
+
+/**
+ * Handles an out-of-character chat message.
+ * @param {Array} args packet arguments
+ */
+export const handleCT = (args: string[]) => {
+ 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;
+ }
+ }
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleCharsCheck.ts b/webAO/packets/handlers/handleCharsCheck.ts
new file mode 100644
index 0000000..2d891ef
--- /dev/null
+++ b/webAO/packets/handlers/handleCharsCheck.ts
@@ -0,0 +1,17 @@
+import { client } from "../../client";
+
+/**
+ * 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
+ */
+export const handleCharsCheck = (args: string[]) => {
+ for (let i = 0; i < client.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";
+ }
+ }
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleDONE.ts b/webAO/packets/handlers/handleDONE.ts
new file mode 100644
index 0000000..3cafd5e
--- /dev/null
+++ b/webAO/packets/handlers/handleDONE.ts
@@ -0,0 +1,16 @@
+import queryParser from "../../utils/queryParser";
+
+let { mode } = queryParser()
+ /**
+ * Handles the handshake completion packet, meaning the player
+ * is ready to select a character.
+ *
+ * @param {Array} args packet arguments
+ */
+export const handleDONE = (_args: string[]) => {
+ document.getElementById("client_loading")!.style.display = "none";
+ if (mode === "watch") {
+ // Spectators don't need to pick a character
+ document.getElementById("client_waiting")!.style.display = "none";
+ }
+ } \ No newline at end of file
diff --git a/webAO/packets/handlers/handleEI.ts b/webAO/packets/handlers/handleEI.ts
new file mode 100644
index 0000000..428baf1
--- /dev/null
+++ b/webAO/packets/handlers/handleEI.ts
@@ -0,0 +1,30 @@
+import { client } from '../../client'
+import { AO_HOST } from '../../client/aoHost';
+import { prepChat, safeTags } from '../../encoding';
+
+/**
+ * Handles incoming evidence information, containing only one evidence
+ * item per packet.
+ *
+ * EI#id#name&description&type&image&##%
+ *
+ * @param {Array} args packet arguments
+ */
+export const handleEI = (args: string[]) => {
+ document.getElementById(
+ "client_loadingtext"
+ )!.innerHTML = `Loading Evidence ${args[1]}/${client.evidence_list_length}`;
+ const evidenceID = Number(args[1]);
+ (<HTMLProgressElement>document.getElementById("client_loadingbar")).value =
+ client.char_list_length + evidenceID;
+
+ const arg = args[2].split("&");
+ client.evidences[evidenceID] = {
+ name: prepChat(arg[0]),
+ desc: prepChat(arg[1]),
+ filename: safeTags(arg[3]),
+ icon: `${AO_HOST}evidence/${encodeURI(arg[3].toLowerCase())}`,
+ };
+
+ client.sender.sendServer("AE" + (evidenceID + 1) + "#%");
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleEM.ts b/webAO/packets/handlers/handleEM.ts
new file mode 100644
index 0000000..5e49ea4
--- /dev/null
+++ b/webAO/packets/handlers/handleEM.ts
@@ -0,0 +1,43 @@
+import { client } from '../../client'
+import { addTrack } from '../../client/addTrack';
+import { createArea } from '../../client/createArea';
+import { fix_last_area } from '../../client/fixLastArea';
+import { isAudio } from '../../client/isAudio';
+import { safeTags } from '../../encoding';
+
+/**
+ * Handles incoming music information, containing multiple entries
+ * per packet.
+ * @param {Array} args packet arguments
+ */
+export const handleEM = (args: string[]) => {
+ document.getElementById("client_loadingtext")!.innerHTML = "Loading Music";
+ if (args[1] === "0") {
+ client.resetMusicList();
+ client.resetAreaList();
+ client.musics_time = false;
+ }
+
+ for (let i = 2; i < args.length - 1; i++) {
+ if (i % 2 === 0) {
+ const trackname = safeTags(args[i]);
+ const trackindex = Number(args[i - 1]);
+ (<HTMLProgressElement>(
+ document.getElementById("client_loadingbar")
+ )).value =
+ client.char_list_length + client.evidence_list_length + trackindex;
+ if (client.musics_time) {
+ addTrack(trackname);
+ } else if (isAudio(trackname)) {
+ client.musics_time = true;
+ fix_last_area();
+ addTrack(trackname);
+ } else {
+ createArea(trackindex, trackname);
+ }
+ }
+ }
+
+ // get the next batch of tracks
+ client.sender.sendServer(`AM#${Number(args[1]) / 10 + 1}#%`);
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleFA.ts b/webAO/packets/handlers/handleFA.ts
new file mode 100644
index 0000000..7a373e8
--- /dev/null
+++ b/webAO/packets/handlers/handleFA.ts
@@ -0,0 +1,15 @@
+import { client } from '../../client'
+import { createArea } from '../../client/createArea';
+import { safeTags } from '../../encoding';
+
+/**
+ * Handles updated area list
+ * @param {Array} args packet arguments
+ */
+export const handleFA = (args: string[]) => {
+ client.resetAreaList();
+
+ for (let i = 1; i < args.length - 1; i++) {
+ createArea(i - 1, safeTags(args[i]));
+ }
+}
diff --git a/webAO/packets/handlers/handleFL.ts b/webAO/packets/handlers/handleFL.ts
new file mode 100644
index 0000000..378d5a9
--- /dev/null
+++ b/webAO/packets/handlers/handleFL.ts
@@ -0,0 +1,48 @@
+import { setExtraFeatures } from "../../client";
+
+
+/**
+ * With this the server tells us which features it supports
+ * @param {Array} args list of features
+ */
+export const handleFL = (args: string[]) => {
+ console.info("Server-supported features:");
+ console.info(args);
+ setExtraFeatures(args);
+
+ if (args.includes("yellowtext")) {
+ const colorselect = <HTMLSelectElement>(
+ 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 = "";
+ }
+}
diff --git a/webAO/packets/handlers/handleFM.ts b/webAO/packets/handlers/handleFM.ts
new file mode 100644
index 0000000..fce10e3
--- /dev/null
+++ b/webAO/packets/handlers/handleFM.ts
@@ -0,0 +1,16 @@
+import { client } from "../../client";
+import { addTrack } from "../../client/addTrack";
+import { safeTags } from "../../encoding";
+
+/**
+ * Handles updated music list
+ * @param {Array} args packet arguments
+ */
+export const handleFM = (args: string[]) => {
+ client.resetMusicList();
+
+ for (let i = 1; i < args.length - 1; i++) {
+ // Check when found the song for the first time
+ addTrack(safeTags(args[i]));
+ }
+}
diff --git a/webAO/packets/handlers/handleHI.ts b/webAO/packets/handlers/handleHI.ts
new file mode 100644
index 0000000..b4f00a8
--- /dev/null
+++ b/webAO/packets/handlers/handleHI.ts
@@ -0,0 +1,14 @@
+import { client } from "../../client";
+const version = process.env.npm_package_version;
+
+
+/**
+ * Handle the player
+ * @param {Array} args packet arguments
+ */
+export const handleHI = (_args: string[]) => {
+ client.sender.sendSelf(`ID#1#webAO#${version}#%`);
+ client.sender.sendSelf(
+ "FL#fastloading#yellowtext#cccc_ic_support#flipping#looping_sfx#effects#%"
+ );
+}
diff --git a/webAO/packets/handlers/handleHP.ts b/webAO/packets/handlers/handleHP.ts
new file mode 100644
index 0000000..f365590
--- /dev/null
+++ b/webAO/packets/handlers/handleHP.ts
@@ -0,0 +1,23 @@
+import { client } from "../../client";
+
+
+ /**
+ * Handles a change in the health bars' states.
+ * @param {Array} args packet arguments
+ */
+export const handleHP = (args: string[]) => {
+ const percent_hp = Number(args[2]) * 10;
+ let healthbox;
+ if (args[1] === "1") {
+ // Def hp
+ client.hp[0] = Number(args[2]);
+ healthbox = document.getElementById("client_defense_hp");
+ } else {
+ // Pro hp
+ client.hp[1] = Number(args[2]);
+ healthbox = document.getElementById("client_prosecutor_hp");
+ }
+ (<HTMLElement>(
+ healthbox.getElementsByClassName("health-bar")[0]
+ )).style.width = `${percent_hp}%`;
+ } \ No newline at end of file
diff --git a/webAO/packets/handlers/handleID.ts b/webAO/packets/handlers/handleID.ts
new file mode 100644
index 0000000..cd7b6ed
--- /dev/null
+++ b/webAO/packets/handlers/handleID.ts
@@ -0,0 +1,24 @@
+import { client, setOldLoading } from "../../client";
+
+
+/**
+ * Identifies the server and issues a playerID
+ * @param {Array} args packet arguments
+ */
+export const handleID = (args: string[]) => {
+ client.playerID = Number(args[1]);
+ const serverSoftware = args[2].split("&")[0];
+ let serverVersion;
+ if (serverSoftware === "serverD") {
+ serverVersion = args[2].split("&")[1];
+ } else if (serverSoftware === "webAO") {
+ setOldLoading(false);
+ client.sender.sendSelf("PN#0#1#%");
+ } else {
+ serverVersion = args[3];
+ }
+
+ if (serverSoftware === "serverD" && serverVersion === "1377.152") {
+ setOldLoading(true);
+ } // bugged version
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleJD.ts b/webAO/packets/handlers/handleJD.ts
new file mode 100644
index 0000000..98d7988
--- /dev/null
+++ b/webAO/packets/handlers/handleJD.ts
@@ -0,0 +1,13 @@
+/**
+* show/hide judge controls
+* @param {number} show either a 1 or a 0
+*/
+export const handleJD = (args: string[]) => {
+ if (Number(args[1]) === 1) {
+ 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";
+ }
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleKB.ts b/webAO/packets/handlers/handleKB.ts
new file mode 100644
index 0000000..b0aa2b2
--- /dev/null
+++ b/webAO/packets/handlers/handleKB.ts
@@ -0,0 +1,13 @@
+import { setBanned } from "../../client";
+import { safeTags } from "../../encoding";
+import { handleBans } from '../../client/handleBans'
+
+/**
+ * Handles the banned packet
+ * this one is sent when you are kicked off the server
+ * @param {Array} args ban reason
+ */
+export const handleKB = (args: string[]) => {
+ handleBans("Banned", safeTags(args[1]));
+ setBanned(true);
+}
diff --git a/webAO/packets/handlers/handleKK.ts b/webAO/packets/handlers/handleKK.ts
new file mode 100644
index 0000000..c8a97b1
--- /dev/null
+++ b/webAO/packets/handlers/handleKK.ts
@@ -0,0 +1,10 @@
+import { safeTags } from "../../encoding";
+import { handleBans } from '../../client/handleBans'
+
+/**
+ * Handles the kicked packet
+ * @param {Array} args kick reason
+ */
+export const handleKK = (args: string[]) => {
+ handleBans("Kicked", safeTags(args[1]));
+}
diff --git a/webAO/packets/handlers/handleLE.ts b/webAO/packets/handlers/handleLE.ts
new file mode 100644
index 0000000..1eaeb27
--- /dev/null
+++ b/webAO/packets/handlers/handleLE.ts
@@ -0,0 +1,35 @@
+import { client } from '../../client'
+import { AO_HOST } from '../../client/aoHost';
+import { prepChat, safeTags } from '../../encoding';
+
+/**
+ * Handles incoming evidence list, all evidences at once
+ * item per packet.
+ *
+ * @param {Array} args packet arguments
+ */
+export const handleLE = (args: string[]) => {
+ client.evidences = [];
+ for (let i = 1; i < args.length - 1; i++) {
+ (<HTMLProgressElement>(
+ document.getElementById("client_loadingbar")
+ )).value = client.char_list_length + i;
+ const arg = args[i].split("&");
+ client.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 <= client.evidences.length; i++) {
+ evidence_box.innerHTML += `<img src="${client.evidences[i - 1].icon}"
+ id="evi_${i}"
+ alt="${client.evidences[i - 1].name}"
+ class="evi_icon"
+ onclick="pickEvidence(${i})">`;
+ }
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleMC.ts b/webAO/packets/handlers/handleMC.ts
new file mode 100644
index 0000000..aeb178d
--- /dev/null
+++ b/webAO/packets/handlers/handleMC.ts
@@ -0,0 +1,43 @@
+import { prepChat } from "../../encoding";
+import { client } from '../../client'
+import { AO_HOST } from "../../client/aoHost";
+import { appendICLog } from '../../client/appendICLog'
+
+/**
+ * Handles a music change to an arbitrary resource.
+ * @param {Array} args packet arguments
+ */
+export const handleMC = (args: string[]) => {
+ 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 = client.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 = client.chars[charID].name;
+ } catch (e) {
+ charID = -1;
+ }
+
+ if (charID >= 0) {
+ musicname = client.chars[charID].name;
+ appendICLog(`${musicname} changed music to ${track}`);
+ } else {
+ appendICLog(`The music was changed to ${track}`);
+ }
+
+ document.getElementById("client_trackstatustext")!.innerText = track;
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleMM.ts b/webAO/packets/handlers/handleMM.ts
new file mode 100644
index 0000000..077140f
--- /dev/null
+++ b/webAO/packets/handlers/handleMM.ts
@@ -0,0 +1,8 @@
+
+/**
+ * Handles the "MusicMode" packet
+ * @param {Array} args packet arguments
+ */
+export const handleMM = (_args: string[]) => {
+ // It's unused nowadays, as preventing people from changing the music is now serverside
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleMS.ts b/webAO/packets/handlers/handleMS.ts
new file mode 100644
index 0000000..92d65db
--- /dev/null
+++ b/webAO/packets/handlers/handleMS.ts
@@ -0,0 +1,165 @@
+import { client, extrafeatures, UPDATE_INTERVAL } from "../../client";
+import { handleCharacterInfo } from "../../client/handleCharacterInfo";
+import { resetICParams } from "../../client/resetICParams";
+import { prepChat, safeTags } from "../../encoding";
+import { handle_ic_speaking } from '../../viewport/utils/handleICSpeaking'
+/**
+ * Handles an in-character chat message.
+ * @param {*} args packet arguments
+ */
+export const handleMS = (args: string[]) => {
+ // TODO: this if-statement might be a bug.
+ if (args[4] !== client.viewport.getChatmsg().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;
+
+ if (char_id < client.char_list_length && char_id >= 0) {
+ if (client.chars[char_id].name !== char_name) {
+ console.info(
+ `${client.chars[char_id].name} is iniediting to ${char_name}`
+ );
+ const chargs = (`${char_name}&` + "iniediter").split("&");
+ handleCharacterInfo(chargs, char_id);
+ }
+ }
+
+ try {
+ msg_nameplate = client.chars[char_id].showname;
+ } catch (e) {
+ msg_nameplate = args[3];
+ }
+
+ try {
+ msg_blips = client.chars[char_id].blips;
+ } catch (e) { }
+
+ try {
+ char_chatbox = client.chars[char_id].chat;
+ } catch (e) {
+ char_chatbox = "default";
+ }
+
+ try {
+ char_muted = client.chars[char_id].muted;
+ } catch (e) {
+ char_muted = false;
+ console.error("we're still missing some character data");
+ }
+
+ if (char_muted === false) {
+ let chatmsg = {
+ deskmod: Number(safeTags(args[1]).toLowerCase()),
+ preanim: safeTags(args[2]).toLowerCase(), // get preanim
+ nameplate: msg_nameplate,
+ chatbox: char_chatbox,
+ name: char_name,
+ sprite: safeTags(args[4]).toLowerCase(),
+ content: prepChat(args[5]), // Escape HTML tags
+ side: args[6].toLowerCase(),
+ sound: safeTags(args[7]).toLowerCase(),
+ blips: safeTags(msg_blips),
+ type: Number(args[8]),
+ charid: char_id,
+ snddelay: Number(args[10]),
+ objection: Number(args[11]),
+ evidence: Number(safeTags(args[12])),
+ flip: Number(args[13]),
+ flash: Number(args[14]),
+ color: Number(args[15]),
+ speed: UPDATE_INTERVAL,
+ };
+
+ if (extrafeatures.includes("cccc_ic_support")) {
+ const extra_cccc = {
+ showname: safeTags(args[16]),
+ other_charid: Number(args[17]),
+ other_name: safeTags(args[18]),
+ other_emote: safeTags(args[19]),
+ self_offset: args[20].split("<and>"), // HACK: here as well, client is fucked and uses this instead of &
+ other_offset: args[21].split("<and>"),
+ other_flip: Number(args[22]),
+ noninterrupting_preanim: Number(args[23]),
+ };
+ 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 === client.charID) {
+ resetICParams();
+ }
+
+ handle_ic_speaking(chatmsg); // no await
+ }
+ }
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handlePN.ts b/webAO/packets/handlers/handlePN.ts
new file mode 100644
index 0000000..1b66fb9
--- /dev/null
+++ b/webAO/packets/handlers/handlePN.ts
@@ -0,0 +1,9 @@
+import { client } from "../../client";
+
+/**
+ * Indicates how many users are on this server
+ * @param {Array} args packet arguments
+ */
+export const handlePN = (_args: string[]) => {
+ client.sender.sendServer("askchaa#%");
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handlePV.ts b/webAO/packets/handlers/handlePV.ts
new file mode 100644
index 0000000..3c669b9
--- /dev/null
+++ b/webAO/packets/handlers/handlePV.ts
@@ -0,0 +1,85 @@
+import { client } from "../../client";
+import fileExists from "../../utils/fileExists";
+import { updateActionCommands } from '../../dom/updateActionCommands'
+import { pickEmotion } from '../../dom/pickEmotion'
+import { AO_HOST } from "../../client/aoHost";
+
+/**
+ * Handles the server's assignment of a character for the player to use.
+ * PV # playerID (unused) # CID # character ID
+ * @param {Array} args packet arguments
+ */
+export const handlePV = async (args: string[]) => {
+ client.charID = Number(args[3]);
+ document.getElementById("client_waiting")!.style.display = "none";
+ document.getElementById("client_charselect")!.style.display = "none";
+
+ const me = client.chars[client.charID];
+ client.selectedEmote = -1;
+ const { emotes } = client;
+ 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 = `<span
+ id="emo_0"
+ alt="unavailable"
+ class="emote_button">No emotes available</span>`;
+ } 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,
+ deskmod: Number(emoteinfo[4]) || 1,
+ 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 += `<img src=${emotes[i].button}
+ id="emo_${i}"
+ alt="${emotes[i].desc}"
+ title="${emotes[i].desc}"
+ class="emote_button"
+ onclick="pickEmotion(${i})">`;
+ } 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";
+ }
+
+}
diff --git a/webAO/packets/handlers/handleRC.ts b/webAO/packets/handlers/handleRC.ts
new file mode 100644
index 0000000..0b5679f
--- /dev/null
+++ b/webAO/packets/handlers/handleRC.ts
@@ -0,0 +1,10 @@
+import { client } from "../../client";
+import vanilla_character_arr from "../../constants/characters.js";
+
+/**
+ * we are asking ourselves what characters there are
+ * @param {Array} args packet arguments
+ */
+export const handleRC = (_args: string[]) => {
+ client.sender.sendSelf(`SC#${vanilla_character_arr.join("#")}#%`);
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleRD.ts b/webAO/packets/handlers/handleRD.ts
new file mode 100644
index 0000000..dde994c
--- /dev/null
+++ b/webAO/packets/handlers/handleRD.ts
@@ -0,0 +1,18 @@
+import { client } from "../../client";
+
+
+/**
+ * we are asking ourselves what characters there are
+ * @param {Array} args packet arguments
+ */
+export const handleRD = (_args: string[]) => {
+ client.sender.sendSelf("BN#gs4#%");
+ client.sender.sendSelf("DONE#%");
+ const ooclog = <HTMLInputElement>document.getElementById("client_ooclog");
+ ooclog.value = "";
+ ooclog.readOnly = false;
+
+ document.getElementById("client_oocinput")!.style.display = "none";
+ document.getElementById("client_replaycontrols")!.style.display =
+ "inline-block";
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleRM.ts b/webAO/packets/handlers/handleRM.ts
new file mode 100644
index 0000000..c18821b
--- /dev/null
+++ b/webAO/packets/handlers/handleRM.ts
@@ -0,0 +1,10 @@
+import {client} from '../../client'
+import vanilla_music_arr from "../../constants/music.js";
+
+ /**
+ * we are asking ourselves what characters there are
+ * @param {Array} args packet arguments
+ */
+export const handleRM = (_args: string[]) => {
+ client.sender.sendSelf(`SM#${vanilla_music_arr.join("#")}#%`);
+ } \ No newline at end of file
diff --git a/webAO/packets/handlers/handleRMC.ts b/webAO/packets/handlers/handleRMC.ts
new file mode 100644
index 0000000..ada1ad2
--- /dev/null
+++ b/webAO/packets/handlers/handleRMC.ts
@@ -0,0 +1,24 @@
+import { client } from '../../client'
+// TODO BUG:
+// this.viewport.music is an array. Therefore you must access elements
+/**
+ * Handles a music change to an arbitrary resource, with an offset in seconds.
+ * @param {Array} args packet arguments
+ */
+export const handleRMC = (args: string[]) => {
+ client.viewport.music.pause();
+ const { music } = client.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
+ );
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleRT.ts b/webAO/packets/handlers/handleRT.ts
new file mode 100644
index 0000000..62ebb1e
--- /dev/null
+++ b/webAO/packets/handlers/handleRT.ts
@@ -0,0 +1,25 @@
+import { client } from "../../client";
+import { initTestimonyUpdater } from '../../viewport/utils/initTestimonyUpdater'
+
+/**
+ * Handles a testimony states.
+ * @param {Array} args packet arguments
+ */
+export const handleRT = (args: string[]) => {
+ const judgeid = Number(args[2]);
+ switch (args[1]) {
+ case "testimony1":
+ client.testimonyID = 1;
+ break;
+ case "testimony2":
+ // Cross Examination
+ client.testimonyID = 2;
+ break;
+ case "judgeruling":
+ client.testimonyID = 3 + judgeid;
+ break;
+ default:
+ console.warn("Invalid testimony");
+ }
+ initTestimonyUpdater();
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleSC.ts b/webAO/packets/handlers/handleSC.ts
new file mode 100644
index 0000000..b42a4cd
--- /dev/null
+++ b/webAO/packets/handlers/handleSC.ts
@@ -0,0 +1,38 @@
+import queryParser from "../../utils/queryParser";
+
+import { client } from '../../client'
+import { handleCharacterInfo } from "../../client/handleCharacterInfo";
+let { mode } = queryParser();
+
+/**
+ * Handles incoming character information, containing all characters
+ * in one packet.
+ * @param {Array} args packet arguments
+ */
+export const handleSC = async (args: string[]) => {
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
+
+ if (mode === "watch") {
+ // Spectators don't need to pick a character
+ document.getElementById("client_charselect")!.style.display = "none";
+ } else {
+ document.getElementById("client_charselect")!.style.display = "block";
+ }
+
+ document.getElementById("client_loadingtext")!.innerHTML =
+ "Loading Characters";
+ for (let i = 1; i < args.length - 1; i++) {
+ document.getElementById(
+ "client_loadingtext"
+ )!.innerHTML = `Loading Character ${i}/${client.char_list_length}`;
+ const chargs = args[i].split("&");
+ const charid = i - 1;
+ (<HTMLProgressElement>(
+ document.getElementById("client_loadingbar")
+ )).value = charid;
+ await sleep(0.1); // TODO: Too many network calls without this. net::ERR_INSUFFICIENT_RESOURCES
+ handleCharacterInfo(chargs, charid);
+ }
+ // We're done with the characters, request the music
+ client.sender.sendServer("RM#%");
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleSI.ts b/webAO/packets/handlers/handleSI.ts
new file mode 100644
index 0000000..b32fbc1
--- /dev/null
+++ b/webAO/packets/handlers/handleSI.ts
@@ -0,0 +1,41 @@
+import { client, extrafeatures, oldLoading } from "../../client";
+
+
+/**
+ * Received when the server announces its server info,
+ * but we use it as a cue to begin retrieving characters.
+ * @param {Array} args packet arguments
+ */
+export const handleSI = (args: string[]) => {
+ client.char_list_length = Number(args[1]);
+ client.char_list_length += 1; // some servers count starting from 0 some from 1...
+ client.evidence_list_length = Number(args[2]);
+ client.music_list_length = Number(args[3]);
+
+ (<HTMLProgressElement>document.getElementById("client_loadingbar")).max =
+ client.char_list_length +
+ client.evidence_list_length +
+ client.music_list_length;
+
+ // create the charselect grid, to be filled by the character loader
+ document.getElementById("client_chartable")!.innerHTML = "";
+
+ for (let i = 0; i < client.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")) {
+ client.sender.sendServer("RC#%");
+ } else {
+ client.sender.sendServer("askchar2#%");
+ }
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleSM.ts b/webAO/packets/handlers/handleSM.ts
new file mode 100644
index 0000000..08bf7e0
--- /dev/null
+++ b/webAO/packets/handlers/handleSM.ts
@@ -0,0 +1,41 @@
+import { client } from '../../client'
+import { addTrack } from '../../client/addTrack'
+import { isAudio } from '../../client/isAudio'
+import { fix_last_area } from '../../client/fixLastArea'
+import { createArea } from '../../client/createArea'
+/**
+ * Handles incoming music information, containing all music in one packet.
+ * @param {Array} args packet arguments
+ */
+export const handleSM = (args: string[]) => {
+ document.getElementById("client_loadingtext")!.innerHTML = "Loading Music ";
+ client.resetMusicList();
+ client.resetAreaList();
+
+ client.musics_time = false;
+
+ for (let i = 1; i < args.length - 1; i++) {
+ // Check when found the song for the first time
+ const trackname = args[i];
+ const trackindex = i - 1;
+ document.getElementById(
+ "client_loadingtext"
+ )!.innerHTML = `Loading Music ${i}/${client.music_list_length}`;
+ (<HTMLProgressElement>(
+ document.getElementById("client_loadingbar")
+ )).value = client.char_list_length + client.evidence_list_length + i;
+ if (client.musics_time) {
+ addTrack(trackname);
+ } else if (isAudio(trackname)) {
+ client.musics_time = true;
+ fix_last_area();
+ addTrack(trackname);
+ } else {
+ createArea(trackindex, trackname);
+ }
+
+ }
+
+ // Music done, carry on
+ client.sender.sendServer("RD#%");
+}
diff --git a/webAO/packets/handlers/handleSP.ts b/webAO/packets/handlers/handleSP.ts
new file mode 100644
index 0000000..e176eeb
--- /dev/null
+++ b/webAO/packets/handlers/handleSP.ts
@@ -0,0 +1,8 @@
+import { updateActionCommands } from '../../dom/updateActionCommands'
+/**
+* position change
+* @param {string} pos new position
+*/
+export const handleSP = (args: string[]) => {
+ updateActionCommands(args[1]);
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleTI.ts b/webAO/packets/handlers/handleTI.ts
new file mode 100644
index 0000000..e418088
--- /dev/null
+++ b/webAO/packets/handlers/handleTI.ts
@@ -0,0 +1,21 @@
+/**
+ * Handles a timer update
+ * @param {Array} args packet arguments
+ */
+export const handleTI = (args: string[]) => {
+ 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";
+ }
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleZZ.ts b/webAO/packets/handlers/handleZZ.ts
new file mode 100644
index 0000000..1c1cb1d
--- /dev/null
+++ b/webAO/packets/handlers/handleZZ.ts
@@ -0,0 +1,23 @@
+import { client } from "../../client";
+import { AO_HOST } from "../../client/aoHost";
+import { prepChat } from "../../encoding";
+
+
+/**
+ * Handles a modcall
+ * @param {Array} args packet arguments
+ */
+export const handleZZ = (args: string[]) => {
+ const oocLog = document.getElementById("client_ooclog")!;
+ oocLog.innerHTML += `$Alert: ${prepChat(args[1])}\r\n`;
+ if (oocLog.scrollTop > oocLog.scrollHeight - 60) {
+ oocLog.scrollTop = oocLog.scrollHeight;
+ }
+
+ client.viewport.getSfxAudio().pause();
+ const oldvolume = client.viewport.getSfxAudio().volume;
+ client.viewport.getSfxAudio().volume = 1;
+ client.viewport.getSfxAudio().src = `${AO_HOST}sounds/general/sfx-gallery.opus`;
+ client.viewport.getSfxAudio().play();
+ client.viewport.getSfxAudio().volume = oldvolume;
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleackMS.ts b/webAO/packets/handlers/handleackMS.ts
new file mode 100644
index 0000000..dcca118
--- /dev/null
+++ b/webAO/packets/handlers/handleackMS.ts
@@ -0,0 +1,8 @@
+import { resetICParams } from '../../client/resetICParams'
+
+/**
+* server got our message
+*/
+export const handleackMS = () => {
+ resetICParams();
+} \ No newline at end of file
diff --git a/webAO/packets/handlers/handleaskchaa.ts b/webAO/packets/handlers/handleaskchaa.ts
new file mode 100644
index 0000000..0f9e730
--- /dev/null
+++ b/webAO/packets/handlers/handleaskchaa.ts
@@ -0,0 +1,10 @@
+import { client } from "../../client";
+import vanilla_character_arr from "../../constants/characters.js";
+
+/**
+ * What? you want a character list from me??
+ * @param {Array} args packet arguments
+ */
+export const handleaskchaa = (_args: string[]) => {
+ client.sender.sendSelf(`SI#${vanilla_character_arr.length}#0#0#%`);
+}
diff --git a/webAO/packets/ms.js b/webAO/packets/ms.js
deleted file mode 100644
index 26851bd..0000000
--- a/webAO/packets/ms.js
+++ /dev/null
@@ -1,28 +0,0 @@
-export default {
- 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,
-};
diff --git a/webAO/packets/packetHandler.ts b/webAO/packets/packetHandler.ts
new file mode 100644
index 0000000..a9b567a
--- /dev/null
+++ b/webAO/packets/packetHandler.ts
@@ -0,0 +1,3 @@
+import { packets } from './packets'
+
+export const packetHandler = new Map(Object.entries(packets)) \ No newline at end of file
diff --git a/webAO/packets/packets.ts b/webAO/packets/packets.ts
new file mode 100644
index 0000000..79c43c1
--- /dev/null
+++ b/webAO/packets/packets.ts
@@ -0,0 +1,86 @@
+import { handleMS } from './handlers/handleMS';
+import { handleCT } from './handlers/handleCT'
+import { handleMC } from './handlers/handleMC'
+import { handleRMC } from './handlers/handleRMC'
+import { handleFL } from './handlers/handleFL'
+import { handleLE } from './handlers/handleLE'
+import { handleEM } from './handlers/handleEM'
+import { handleEI } from './handlers/handleEI'
+import { handleSC } from './handlers/handleSC'
+import { handleCI } from './handlers/handleCI'
+import { handleFM } from './handlers/handleFM'
+import { handleFA } from './handlers/handleFA'
+import { handleSM } from './handlers/handleSM'
+import { handleMM } from './handlers/handleMM'
+import { handleBD } from './handlers/handleBD'
+import { handleBB } from './handlers/handleBB'
+import { handleKB } from './handlers/handleKB'
+import { handleKK } from './handlers/handleKK'
+import { handleDONE } from './handlers/handleDONE'
+import { handleBN } from './handlers/handleBN'
+import { handleHP } from './handlers/handleHP'
+import { handleRT } from './handlers/handleRT'
+import { handleTI } from './handlers/handleTI'
+import { handleZZ } from './handlers/handleZZ'
+import { handleHI } from './handlers/handleHI'
+import { handleID } from './handlers/handleID'
+import { handlePN } from './handlers/handlePN'
+import { handleSI } from './handlers/handleSI'
+import { handleARUP } from './handlers/handleARUP'
+import { handleaskchaa } from './handlers/handleaskchaa'
+import { handleCC } from './handlers/handleCC'
+import { handleRC } from './handlers/handleRC'
+import { handleRM } from './handlers/handleRM'
+import { handleRD } from './handlers/handleRD'
+import { handleCharsCheck } from './handlers/handleCharsCheck'
+import { handlePV } from './handlers/handlePV'
+import { handleASS } from './handlers/handleASS'
+import { handleackMS } from './handlers/handleackMS'
+import { handleSP } from './handlers/handleSP'
+import { handleJD } from './handlers/handleJD'
+
+export const packets = {
+ "MS": handleMS,
+ "CT": handleCT,
+ "MC": handleMC,
+ "RMC": handleRMC,
+ "CI": handleCI,
+ "SC": handleSC,
+ "EI": handleEI,
+ "FL": handleFL,
+ "LE": handleLE,
+ "EM": handleEM,
+ "FM": handleFM,
+ "FA": handleFA,
+ "SM": handleSM,
+ "MM": handleMM,
+ "BD": handleBD,
+ "BB": handleBB,
+ "KB": handleKB,
+ "KK": handleKK,
+ "DONE": handleDONE,
+ "BN": handleBN,
+ "HP": handleHP,
+ "RT": handleRT,
+ "TI": handleTI,
+ "ZZ": handleZZ,
+ "HI": handleHI,
+ "ID": handleID,
+ "PN": handlePN,
+ "SI": handleSI,
+ "ARUP": handleARUP,
+ "askchaa": handleaskchaa,
+ "CC": handleCC,
+ "RC": handleRC,
+ "RM": handleRM,
+ "RD": handleRD,
+ "CharsCheck": handleCharsCheck,
+ "PV": handlePV,
+ "ASS": handleASS,
+ "ackMS": handleackMS,
+ "SP": handleSP,
+ "JD": handleJD,
+ "decryptor": () => { },
+ "CHECK": () => { },
+ "CH": () => { },
+} \ No newline at end of file
diff --git a/webAO/styles/chatbox/aa.css b/webAO/styles/chatbox/aa.css
index 33428ae..a504daf 100644
--- a/webAO/styles/chatbox/aa.css
+++ b/webAO/styles/chatbox/aa.css
@@ -86,7 +86,6 @@
text-justify: distribute;
line-height: 100%;
margin: 0;
- margin-top: 2.75%;
}
#client_chat {
@@ -139,4 +138,12 @@
100% {
right: 0;
}
+}
+
+#client_trackstatus {
+ display: none;
+}
+
+#client_clock {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/acww.css b/webAO/styles/chatbox/acww.css
index 9818b4e..dd899b9 100644
--- a/webAO/styles/chatbox/acww.css
+++ b/webAO/styles/chatbox/acww.css
@@ -2,6 +2,54 @@
color: #000;
}
+.text_green {
+ color: #009600;
+}
+
+.text_red {
+ color: #ff0859;
+}
+
+.text_orange {
+ color: #610000;
+}
+
+.text_blue {
+ color: #7900ff;
+}
+
+.text_yellow {
+ color: #ff0;
+}
+
+.text_pink {
+ color: #ff51ff;
+}
+
+.text_cyan {
+ color: #0038ff;
+}
+
+.text_grey {
+ color: #bbb;
+}
+
+/* Webfont CSS setup for Igiari by Caveras */
+
+@font-face {
+ font-family:'Igiari Cyrillic';
+ src: url('../igiari/igiari-cyrillic.ttf');
+ font-weight:normal;
+ font-style:normal;
+}
+
+@font-face {
+ font-family:'Ace Name';
+ src: url('./ace-name.ttf');
+ font-weight:normal;
+ font-style:normal;
+}
+
#client_chatcontainer {
display: block;
position: absolute;
@@ -21,7 +69,7 @@
min-width: 11%;
font-size: 0.8em;
background: #d8f8a0;
- color: #004000;
+ color: #040;
margin: 0;
padding-top: 0.4%;
padding-left: 1.955%;
@@ -29,11 +77,11 @@
border-color: #b0f818;
border-style: solid;
border-width: 0.3em 0.6em;
- border-radius: 10%/50%;
+ border-radius: 15%/50%;
box-shadow: 0 0.3em #70c020;
line-height: 100%;
z-index: 1;
- font-family: sans-serif;
+ font-family: "Ace Name", "Igiari Cyrillic", "MS PGothic", "MS UI Gothic", "MS Sans Serif", "Hiragino Maru Gothic Pro", "Mitra Mono", "Mukti Narrow", "Meera", "Khmer OS", "FreeSans", "Gargi", sans-serif;
}
#client_inner_name {
@@ -51,11 +99,6 @@
width: 100%;
height: 87%;
margin: auto;
- /*border-color: darkgray;
- border-style: solid;
- border-width: 0.15em;
- border-radius: 0.15em;
- background-color: white;*/
background-image: url("acww.svg");
background-size: cover;
background-repeat: no-repeat;
@@ -64,11 +107,11 @@
text-align: left;
overflow: hidden;
scroll-behavior: smooth;
- font-family: sans-serif;
+ font-family: "Igiari Cyrillic", "MS PGothic", "MS UI Gothic", "MS Sans Serif", "Hiragino Maru Gothic Pro", "Mitra Mono", "Mukti Narrow", "Meera", "Khmer OS", "FreeSans", "Gargi", sans-serif;
}
#client_inner_chat {
- padding: 5% 12%;
+ padding: 5% 16%;
margin: 0;
line-height: 100%;
}
@@ -84,7 +127,7 @@
height: 1.5em;
background-image: url("chatwaiting_acww.svg");
background-size: contain;
- animation: idling 0.4s linear infinite;
+ animation: idling 0.4s steps(1) infinite;
}
@keyframes idling {
@@ -99,4 +142,81 @@
100% {
bottom: 12%;
}
+}
+
+#client_trackstatus {
+ display: none;
+}
+
+#client_clock_date {
+ position: absolute;
+ width: 13%;
+ height: 4%;
+ right: 0;
+ top: 1%;
+ text-align: left;
+ color: #7d5500;
+ background: linear-gradient(
+ to bottom,
+ #ffef55,
+ #ffef55 20%,
+ #ffcf3c 20%,
+ #ffcf3c 40%,
+ #ffef55 40%,
+ #ffef55 60%,
+ #ffcf3c 60%,
+ #ffcf3c 80%,
+ #ffef55 80%,
+ #ffef55 100%
+ );
+ border: 0.1em solid #ae4500;
+ border-radius: 50%;
+ border-bottom: none;
+ font-weight: bold;
+ padding-left: 1.5em;
+ padding-top: 0.5em;
+}
+
+#client_clock_month {
+ display: none;
+}
+
+#client_clock_weekday {
+ position: absolute;
+ width: 2.5%;
+ height: 3%;
+ right: 1%;
+ top: 2%;
+ color: white;
+ background: #00b63c;
+ background-size: 100% 40%;
+ border: 0.1em solid #00b63c;
+ border-radius: 100%;
+}
+
+#client_clock_time {
+ position: absolute;
+ width: 16%;
+ height: 4%;
+ right: 2%;
+ top: 4%;
+ text-align: center;
+ color: #5d5500;
+ background: linear-gradient(
+ to bottom,
+ rgba(255,255,0,0),
+ rgba(255,255,0,0) 20%,
+ #ffcf3c 20%,
+ #ffcf3c 40%,
+ #ffef55 40%,
+ #ffef55 60%,
+ #ffcf3c 60%,
+ #ffcf3c 80%,
+ #ffef55 80%,
+ #ffef55 100%
+ );
+ border: 0.1em solid #ae4500;
+ border-radius: 50%;
+ border-top: none;
+ padding-top: 0.5em;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/bricks.svg b/webAO/styles/chatbox/bricks.svg
new file mode 100644
index 0000000..6baca51
--- /dev/null
+++ b/webAO/styles/chatbox/bricks.svg
@@ -0,0 +1,11 @@
+<svg id="SVGRoot" width="16px" height="16px" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <rect width="16" height="16" fill="#a55"/>
+ <g transform="matrix(.9814 0 0 .9816 .04907 .04908)" fill="none" stroke="#744">
+ <path d="m0 0.2h16" stroke-width=".5"/>
+ <path d="m-0.05 16h16.3" stroke-width=".5"/>
+ <path d="m-0.05 8.1h16.3" stroke-width=".9"/>
+ <path d="m8.101 8.1v8.15" stroke-width=".9px"/>
+ <path d="m0.2047-0.05v8.15" stroke-width=".5"/>
+ <path d="m16-0.05v8.15" stroke-width=".5"/>
+ </g>
+</svg>
diff --git a/webAO/styles/chatbox/chat999.css b/webAO/styles/chatbox/chat999.css
index 55c7194..de20d28 100644
--- a/webAO/styles/chatbox/chat999.css
+++ b/webAO/styles/chatbox/chat999.css
@@ -117,4 +117,12 @@
100% {
right: 0;
}
+}
+
+#client_trackstatus {
+ display: none;
+}
+
+#client_clock {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/chatdd.css b/webAO/styles/chatbox/chatdd.css
index 7fb6bee..d1561a4 100644
--- a/webAO/styles/chatbox/chatdd.css
+++ b/webAO/styles/chatbox/chatdd.css
@@ -100,4 +100,12 @@
100% {
right: 0;
}
+}
+
+#client_trackstatus {
+ display: none;
+}
+
+#client_clock {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/chatdr2.css b/webAO/styles/chatbox/chatdr2.css
index 73187f1..59b11dc 100644
--- a/webAO/styles/chatbox/chatdr2.css
+++ b/webAO/styles/chatbox/chatdr2.css
@@ -45,6 +45,7 @@
#client_name {
display: block;
+ position: absolute;
height: 100%;
width: 7%;
text-align: left;
@@ -55,8 +56,6 @@
box-shadow: 0.3em 0px 0 #ff9700;
left: 0;
bottom: 0;
- position: absolute;
- z-index: 1;
}
#client_inner_name {
@@ -98,4 +97,31 @@
#client_chatwaiting {
display: none;
+}
+
+#client_trackstatus {
+ display: block;
+ position: absolute;
+ width: 30%;
+ height: 35%;
+ top: 0;
+ left: 0;
+ background-image: url("music_dr2.svg");
+ background-size: contain;
+ background-repeat: no-repeat;
+}
+
+#client_trackstatustext {
+ position: absolute;
+ top: 6%;
+ left: 25%;
+ width: 50%;
+ color: #f84f00;
+ font-family: monospace;
+ font-size: 1.2em;
+ white-space: nowrap;
+}
+
+#client_trackstatus {
+ display: fuck;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/chatfuture.css b/webAO/styles/chatbox/chatfuture.css
index 8b2f6a4..cd638f4 100644
--- a/webAO/styles/chatbox/chatfuture.css
+++ b/webAO/styles/chatbox/chatfuture.css
@@ -126,4 +126,12 @@
100% {
right: 0;
}
+}
+
+#client_trackstatus {
+ display: none;
+}
+
+#client_clock {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/chatp3.css b/webAO/styles/chatbox/chatp3.css
index c8ed3f8..8c6a39d 100644
--- a/webAO/styles/chatbox/chatp3.css
+++ b/webAO/styles/chatbox/chatp3.css
@@ -110,4 +110,8 @@
bottom: 0.5em;
opacity: 1;
}
+}
+
+#client_trackstatus {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/chatplvsaa.css b/webAO/styles/chatbox/chatplvsaa.css
index 23a8ea0..a656e9b 100644
--- a/webAO/styles/chatbox/chatplvsaa.css
+++ b/webAO/styles/chatbox/chatplvsaa.css
@@ -1,5 +1,5 @@
.text_white {
- color: #fff;
+ color: #120f09;
}
.text_blue {
@@ -53,33 +53,39 @@
#client_name {
display: block;
- left: 1%;
+ left: 2%;
top: 0;
height: 20%;
min-width: 15%;
padding: 0px 6px;
- border: 2px ridge #b1822d;
- border-radius: 0.4em;
- background: #783500;
+ image-rendering: crisp-edges;
+ border-image-source: url("plvspw_name.png");
+ border-image-slice: 6 fill;
+ border-image-width: 0.4em;
position: absolute;
+ font-weight: bold;
+ -webkit-text-stroke: 0.02em #000;
z-index: 1;
}
#client_inner_name {
margin: 1px;
+ padding: 0 0.4em;
}
#client_chat {
font-size: 1em;
display: block;
width: 99%;
- width: calc(100% - 4px);
+ width: calc(100% - 0.4em);
margin: auto;
height: 80%;
- border: 2px ridge #d9a63b;
- border-radius: 0.5em;
- background-color: rgba(148,96,0,0.6);
+ image-rendering: crisp-edges;
+ border-image-source: url("plvspw.png");
+ border-image-slice: 10 fill;
+ border-image-width: 0.4em;
bottom: 0;
+ left: 0.2em;
position: absolute;
word-break: keep-all;
word-wrap: break-word;
@@ -90,10 +96,18 @@
}
#client_inner_chat {
- margin: 6px;
- padding: 6px 20px;
+ margin: 0.1em;
+ padding: 0.3em 0.6em;
}
#client_chatwaiting {
display: none;
+}
+
+#client_trackstatus {
+ display: none;
+}
+
+#client_clock {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/chatwaiting_acww.svg b/webAO/styles/chatbox/chatwaiting_acww.svg
index 0f86322..057bf37 100644
--- a/webAO/styles/chatbox/chatwaiting_acww.svg
+++ b/webAO/styles/chatbox/chatwaiting_acww.svg
@@ -1 +1,50 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="17" viewBox="0 0 4.2 4.5"><image width="4.2" height="4.5" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAARCAYAAADUryzEAAAABHNCSVQICAgIfAhkiAAAALNJREFU OI2VkrsVwyAMRZ99UlBmlJSUKT2CR3DJKCkzgkdwSekxKD2COqeC8JEsrMrC8O4VhwGv7cTNov2d vh/1glbG+qIfucXewwAwENFprO+yMNZjeR6tQfzpArEHXSDRcgTKO8hDXKCir+lphGgAAMtqm03f eWcDPtv8H0G7A46eRuBoUp/TmwDJQqKzBjlVo7MBtcUVXTTopYsB0UKjA9k7kMpNq0gXDe6UGhCp HB0Afq/yUx1qGz9eAAAAAElFTkSuQmCC "/><path d="M0.9 0.9H4.1l-1.6 3.6z" style="fill:#6840a0;stroke-width:0.3"/><path d="M0.3 0.3 1.7 3.3 3.2 0.3Z" style="fill:#68d8f0;stroke-width:0.3px"/><path d="M0.3 0.3H3.2L1.7 1.5Z" style="fill:#fff;stroke-width:0.3"/><path d="M0.1 0.1H3.3l-1.6 3.4z" style="fill:none;stroke-width:0.3;stroke:#0030b8"/></svg> \ No newline at end of file
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ width="16"
+ height="17"
+ viewBox="0 0 4.2 4.5"
+ version="1.1"
+ id="svg12"
+ sodipodi:docname="chatwaiting_acww.svg"
+ inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <defs
+ id="defs16" />
+ <sodipodi:namedview
+ id="namedview14"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ showgrid="false"
+ inkscape:zoom="51.352941"
+ inkscape:cx="7.9936999"
+ inkscape:cy="8.5"
+ inkscape:window-width="3840"
+ inkscape:window-height="2097"
+ inkscape:window-x="-8"
+ inkscape:window-y="-8"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg12" />
+ <path
+ d="M0.9 0.9H4.1l-1.6 3.6z"
+ style="fill:#6840a0;stroke-width:0.3"
+ id="path4" />
+ <path
+ d="M0.3 0.3 1.7 3.3 3.2 0.3Z"
+ style="fill:#68d8f0;stroke-width:0.3px"
+ id="path6" />
+ <path
+ d="M0.3 0.3H3.2L1.7 1.5Z"
+ style="fill:#fff;stroke-width:0.3"
+ id="path8" />
+ <path
+ d="M0.1 0.1H3.3l-1.6 3.4z"
+ style="fill:none;stroke-width:0.3;stroke:#0030b8"
+ id="path10" />
+</svg>
diff --git a/webAO/styles/chatbox/ddlc.css b/webAO/styles/chatbox/ddlc.css
index 01d30e8..0b019fb 100644
--- a/webAO/styles/chatbox/ddlc.css
+++ b/webAO/styles/chatbox/ddlc.css
@@ -1,46 +1,37 @@
.text_white {
color: #fff;
- -webkit-text-stroke: 0.04em #523643;
}
.text_blue {
color: #6bc6f7;
- -webkit-text-stroke: 0.04em #523643;
}
.text_green {
color: #00f700;
- -webkit-text-stroke: 0.04em #523643;
}
.text_red {
color: #f00;
- -webkit-text-stroke: 0.04em #523643;
}
.text_orange {
color: #f77339;
- -webkit-text-stroke: 0.04em #523643;
}
.text_yellow {
color: #ff0;
- -webkit-text-stroke: 0.04em #523643;
}
.text_pink {
color: #ffc0cb;
- -webkit-text-stroke: 0.04em #523643;
}
.text_cyan {
color: #0ff;
- -webkit-text-stroke: 0.04em #523643;
}
.text_grey {
color: #bbb;
- -webkit-text-stroke: 0.04em #523643;
}
#client_chatcontainer {
@@ -97,6 +88,7 @@
font-family: "Aller", "OpenSans", sans-serif;
font-size: 1em;
font-weight: 600;
+ -webkit-text-stroke: 0.04em #523643;
word-break: keep-all;
overflow-wrap: break-word;
text-align: left;
@@ -131,4 +123,12 @@
100% {
right: 0;
}
+}
+
+#client_trackstatus {
+ display: none;
+}
+
+#client_clock {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/dgs.css b/webAO/styles/chatbox/dgs.css
index 654b86c..77b8db0 100644
--- a/webAO/styles/chatbox/dgs.css
+++ b/webAO/styles/chatbox/dgs.css
@@ -133,3 +133,11 @@
right: 0.6em;
}
}
+
+#client_trackstatus {
+ display: none;
+}
+
+#client_clock {
+ display: none;
+} \ No newline at end of file
diff --git a/webAO/styles/chatbox/dr1.css b/webAO/styles/chatbox/dr1.css
index 7696b26..ced1932 100644
--- a/webAO/styles/chatbox/dr1.css
+++ b/webAO/styles/chatbox/dr1.css
@@ -155,4 +155,8 @@
@keyframes marquee {
from { text-indent: 0% }
to { text-indent: -125% }
+}
+
+#client_clock {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/drae.css b/webAO/styles/chatbox/drae.css
index 14bde1d..d5b9fa0 100644
--- a/webAO/styles/chatbox/drae.css
+++ b/webAO/styles/chatbox/drae.css
@@ -99,4 +99,12 @@
#client_chatwaiting {
display: none;
+}
+
+#client_trackstatus {
+ display: none;
+}
+
+#client_clock {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/drv3.css b/webAO/styles/chatbox/drv3.css
index 2a9e6d3..c7eb957 100644
--- a/webAO/styles/chatbox/drv3.css
+++ b/webAO/styles/chatbox/drv3.css
@@ -60,7 +60,6 @@
#client_inner_name {
transform: skew(-15deg);
- padding: 0 2%;
margin: 1px;
}
@@ -95,4 +94,27 @@
#client_chatwaiting {
display: none;
+}
+
+#client_trackstatus {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 10%;
+ background-image: url('drv3trackstatus.png');
+ background-size: contain;
+ background-repeat: no-repeat;
+}
+
+#client_trackstatustext {
+ position: absolute;
+ left: 0;
+ top: 10%;
+ background-image: url('drv3trackstatustext.png');
+ background-size: contain;
+ background-repeat: no-repeat;
+}
+
+#client_clock {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/drv3chatbox.png b/webAO/styles/chatbox/drv3chatbox.png
index ecf5e1f..224e2c5 100644
--- a/webAO/styles/chatbox/drv3chatbox.png
+++ b/webAO/styles/chatbox/drv3chatbox.png
Binary files differ
diff --git a/webAO/styles/chatbox/drv3trackstatus.png b/webAO/styles/chatbox/drv3trackstatus.png
new file mode 100644
index 0000000..200ef18
--- /dev/null
+++ b/webAO/styles/chatbox/drv3trackstatus.png
Binary files differ
diff --git a/webAO/styles/chatbox/drv3trackstatustext.png b/webAO/styles/chatbox/drv3trackstatustext.png
new file mode 100644
index 0000000..6bbe27f
--- /dev/null
+++ b/webAO/styles/chatbox/drv3trackstatustext.png
Binary files differ
diff --git a/webAO/styles/chatbox/ff.css b/webAO/styles/chatbox/ff.css
index d0b00b8..942794b 100644
--- a/webAO/styles/chatbox/ff.css
+++ b/webAO/styles/chatbox/ff.css
@@ -100,4 +100,12 @@
padding: 0.4% 2.8%;
margin: 1px;
line-height: 100%;
+}
+
+#client_trackstatus {
+ display: none;
+}
+
+#client_clock {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/halla.css b/webAO/styles/chatbox/halla.css
index 53622a6..0932937 100644
--- a/webAO/styles/chatbox/halla.css
+++ b/webAO/styles/chatbox/halla.css
@@ -132,4 +132,12 @@
50% {
color: #fff;
}
+}
+
+#client_trackstatus {
+ display: none;
+}
+
+#client_clock {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/homestuck.css b/webAO/styles/chatbox/homestuck.css
index aea8f99..e6eea68 100644
--- a/webAO/styles/chatbox/homestuck.css
+++ b/webAO/styles/chatbox/homestuck.css
@@ -72,4 +72,12 @@
position: absolute;
left: 2%;
bottom: 2%;
+}
+
+#client_trackstatus {
+ display: none;
+}
+
+#client_clock {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/key.css b/webAO/styles/chatbox/key.css
index 43efed4..77f2e99 100644
--- a/webAO/styles/chatbox/key.css
+++ b/webAO/styles/chatbox/key.css
@@ -47,6 +47,7 @@
width: 100%;
filter: none;
font-family: "Verdana";
+ transition: height 1s linear;
}
#client_chatdecoration {
@@ -133,4 +134,74 @@
100% {
transform: rotate(90deg) scaleY(1);
}
+}
+
+#client_trackstatus {
+ display: none;
+}
+
+#client_clock {
+ position: absolute;
+ top: 1%;
+ left: 1%;
+ width: 12%;
+ height: 15%;
+ background: url("bricks.svg");
+ background-size: 1.5em;
+ border: #ffbe9f 3px ridge;
+ border-radius: 100%;
+ font-family: serif;
+}
+
+#client_clock_month {
+ position: absolute;
+ width: 90%;
+ height: 15%;
+ left: 5%;
+ top: 8%;
+ margin: 0;
+ color: #52443c;
+ background: #ffb183;
+ border: 2px white outset;
+ border-radius: 100%;
+ text-shadow: -1px -1px 0 #d7e5d9, 1px -1px 0 #d7e5d9, -1px 1px 0 #d7e5d9, 1px 1px 0 #d7e5d9;
+ font-size: small;
+ line-height: 1;
+}
+
+#client_clock_date {
+ position: absolute;
+ width: 80%;
+ height: 80%;
+ left: 10%;
+ top: 10%;
+ margin: 0;
+ color: #ffe;
+ text-shadow: -1px -1px 0 #78320b, 1px -1px 0 #78320b, -1px 1px 0 #78320b, 1px 1px 0 #78320b;
+ font-size: 3em;
+ background: rgba(255,255,255,0.7);
+ border-radius: 100%;
+ line-height: 1.8;
+}
+
+#client_clock_weekday {
+ position: absolute;
+ width: 25%;
+ height: 25%;
+ right: 0;
+ bottom: 0;
+ margin: 0;
+ color: #ccc;
+ text-shadow: -1px -1px 0 #333, 1px -1px 0 #333, -1px 1px 0 #333, 1px 1px 0 #333;
+ text-decoration: underline;
+ font-size: smaller;
+ font-weight: bold;
+ background-color: #c7856f;
+ border: #ffc89f 3px ridge;
+ border-radius: 100%;
+ line-height: 2;
+}
+
+#client_clock_time {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/legacy.css b/webAO/styles/chatbox/legacy.css
index 3fce78f..debbcb3 100644
--- a/webAO/styles/chatbox/legacy.css
+++ b/webAO/styles/chatbox/legacy.css
@@ -86,4 +86,12 @@
#client_chatwaiting {
display: none;
+}
+
+#client_trackstatus {
+ display: none;
+}
+
+#client_clock {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/music_dr2.svg b/webAO/styles/chatbox/music_dr2.svg
new file mode 100644
index 0000000..189c45e
--- /dev/null
+++ b/webAO/styles/chatbox/music_dr2.svg
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="128"
+ height="128"
+ viewBox="0 0 33.866666 33.866666"
+ version="1.1"
+ id="svg5"
+ inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
+ sodipodi:docname="clock_dr2.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview7"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:document-units="px"
+ showgrid="true"
+ units="px"
+ inkscape:zoom="6.2217172"
+ inkscape:cx="86.873123"
+ inkscape:cy="58.263658"
+ inkscape:window-width="1997"
+ inkscape:window-height="1229"
+ inkscape:window-x="1151"
+ inkscape:window-y="656"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="layer1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid21144" />
+ </sodipodi:namedview>
+ <defs
+ id="defs2" />
+ <g
+ inkscape:label="Ebene 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <path
+ style="fill:#ffd800;stroke:#000000;stroke-width:0.13229166;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.49615383;stroke-miterlimit:4;stroke-dasharray:none;fill-opacity:1"
+ d="M 0,0 H 33.866666 L 0,33.866666 Z"
+ id="path92"
+ sodipodi:nodetypes="cccc" />
+ <path
+ style="fill:#343434;fill-opacity:1;stroke:#ffd800;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 7.5406249,29.368749 c -1.4552084,0 -4.6302083,-0.529166 -4.6302083,-4.233333 0.2645833,-2.645833 1.8520833,-1.5875 1.3229166,-5.027083 C 3.7041666,18.785416 3.9687499,17.4625 6.3499999,16.66875 L 6.6145832,7.4083332 H 6.0854165 V 6.8791665 H 5.5562499 v -0.79375 H 6.0854165 V 5.0270832 H 5.5562499 v -0.79375 H 6.0854165 V 3.7041666 L 7.5406249,2.6458333 8.9958332,3.7041666 v 0.5291666 h 0.5291666 v 0.79375 H 8.9958332 v 1.0583333 h 0.5291666 v 0.79375 H 8.9958332 V 7.4083332 H 8.4666665 L 8.7312498,16.66875 c 2.3812502,0.79375 2.6458332,2.116666 2.1166662,3.439583 -0.529166,3.439583 1.058334,2.38125 1.587503,5.027084 -3e-6,3.968749 -3.1750025,4.233332 -4.8947941,4.233332 z"
+ id="path800"
+ sodipodi:nodetypes="ccccccccccccccccccccccccccccccc" />
+ <ellipse
+ style="fill:#ffd800;fill-opacity:1;stroke:#343434;stroke-width:0.240384;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path16494"
+ cx="7.6729164"
+ cy="20.902082"
+ rx="1.8641829"
+ ry="1.8641828" />
+ </g>
+</svg>
diff --git a/webAO/styles/chatbox/n64zelda.css b/webAO/styles/chatbox/n64zelda.css
index 5c203b8..35187f0 100644
--- a/webAO/styles/chatbox/n64zelda.css
+++ b/webAO/styles/chatbox/n64zelda.css
@@ -106,4 +106,12 @@
color: #3282ff;
text-shadow: 0 0 0.2em #3282ff;
}
+}
+
+#client_trackstatus {
+ display: none;
+}
+
+#client_clock {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/p4.css b/webAO/styles/chatbox/p4.css
index 3128d66..7389fae 100644
--- a/webAO/styles/chatbox/p4.css
+++ b/webAO/styles/chatbox/p4.css
@@ -108,4 +108,8 @@
#client_chatwaiting {
display: none;
+}
+
+#client_trackstatus {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/p5.css b/webAO/styles/chatbox/p5.css
index 5df386a..da27529 100644
--- a/webAO/styles/chatbox/p5.css
+++ b/webAO/styles/chatbox/p5.css
@@ -104,4 +104,8 @@
100% {
right: 0;
}
+}
+
+#client_trackstatus {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/papermario.css b/webAO/styles/chatbox/papermario.css
index 1d15590..587fcd1 100644
--- a/webAO/styles/chatbox/papermario.css
+++ b/webAO/styles/chatbox/papermario.css
@@ -90,4 +90,12 @@
to {
transform: rotate(20deg);
}
+}
+
+#client_trackstatus {
+ display: none;
+}
+
+#client_clock {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/plvspw.png b/webAO/styles/chatbox/plvspw.png
new file mode 100644
index 0000000..ba6a7e8
--- /dev/null
+++ b/webAO/styles/chatbox/plvspw.png
Binary files differ
diff --git a/webAO/styles/chatbox/plvspw_name.png b/webAO/styles/chatbox/plvspw_name.png
new file mode 100644
index 0000000..d9fc4e1
--- /dev/null
+++ b/webAO/styles/chatbox/plvspw_name.png
Binary files differ
diff --git a/webAO/styles/chatbox/trilogy.css b/webAO/styles/chatbox/trilogy.css
index bc455e9..05b98b7 100644
--- a/webAO/styles/chatbox/trilogy.css
+++ b/webAO/styles/chatbox/trilogy.css
@@ -109,4 +109,12 @@
100% {
right: 0.6em;
}
+}
+
+#client_trackstatus {
+ display: none;
+}
+
+#client_clock {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/whentheycry.css b/webAO/styles/chatbox/whentheycry.css
index ba11f02..1a08e27 100644
--- a/webAO/styles/chatbox/whentheycry.css
+++ b/webAO/styles/chatbox/whentheycry.css
@@ -103,4 +103,12 @@
left: 0.2em;
top: 0;
}
+}
+
+#client_trackstatus {
+ display: none;
+}
+
+#client_clock {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/yakuza.css b/webAO/styles/chatbox/yakuza.css
index 683e11b..40da26c 100644
--- a/webAO/styles/chatbox/yakuza.css
+++ b/webAO/styles/chatbox/yakuza.css
@@ -100,4 +100,12 @@
background-image: url("x_button.svg");
background-size: contain;
transition: opacity 0.2s;
+}
+
+#client_trackstatus {
+ display: none;
+}
+
+#client_clock {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/chatbox/yttd.css b/webAO/styles/chatbox/yttd.css
index c7e327b..b6bebc7 100644
--- a/webAO/styles/chatbox/yttd.css
+++ b/webAO/styles/chatbox/yttd.css
@@ -111,4 +111,12 @@
100% {
bottom: 2px;
}
+}
+
+#client_trackstatus {
+ display: none;
+}
+
+#client_clock {
+ display: none;
} \ No newline at end of file
diff --git a/webAO/styles/client.css b/webAO/styles/client.css
index e31a8e8..31bc64b 100644
--- a/webAO/styles/client.css
+++ b/webAO/styles/client.css
@@ -1,3 +1,10 @@
+.lm_item .lm_header .lm_controls .lm_maximise {
+ top: 0;
+ right: 0;
+ height: 20px;
+ width: 20px;
+}
+
@keyframes error_blink {
0% {
color: #fff;
diff --git a/webAO/styles/effects/rain.css b/webAO/styles/effects/rain.css
new file mode 100644
index 0000000..846b35e
--- /dev/null
+++ b/webAO/styles/effects/rain.css
@@ -0,0 +1,23 @@
+#client_fg {
+ overflow: hidden;
+}
+
+#client_fg p {
+ position: absolute;
+ bottom: 100%;
+ width: 2px;
+ height: 2em;
+ transform: rotate(10deg);
+ background: linear-gradient(rgba(200,200,200,0) 0%, rgba(200,200,200,0.5) 20%, rgba(250,250,250,0.6) 100%);
+ animation: falling 1s linear infinite;
+}
+
+@keyframes falling {
+ 0% {
+ bottom: 100%;
+
+ }
+ 100% {
+ bottom: -10%;
+ }
+} \ No newline at end of file
diff --git a/webAO/utils/__tests__/paths.test.ts b/webAO/utils/__tests__/paths.test.ts
new file mode 100644
index 0000000..4f41d09
--- /dev/null
+++ b/webAO/utils/__tests__/paths.test.ts
@@ -0,0 +1,13 @@
+import {getFilenameFromPath} from '../paths'
+jest.mock('../fileExists')
+
+describe('getFilenameFromPath', () => {
+ const EXAMPLE_PATH = "localhost/stoneddiscord/assets.png"
+ it('Should get the last value from a path', async () => {
+ const actual = getFilenameFromPath(EXAMPLE_PATH);
+ const expected = 'assets.png';
+ expect(actual).toBe(expected);
+ });
+})
+
+
diff --git a/webAO/utils/calculateApngLength.js b/webAO/utils/calculateApngLength.js
index 932f581..d6a40b6 100644
--- a/webAO/utils/calculateApngLength.js
+++ b/webAO/utils/calculateApngLength.js
@@ -13,13 +13,18 @@ const calculateApngLength = (apngFile) => {
&& d[i + 2] === 0x54
&& d[i + 3] === 0x4C) {
// numerator and denominator
- const delay = ((d[i + 21] / d[i + 23]) * 1000);
-
+ const delay_num = Number(d[i + 23]);
+ const delay_den = Number(d[i + 25]);
+ let delay;
// minimum is 100ms
- duration += delay < 100 ? 100 : delay;
+ if (delay_den == 0)
+ delay = delay_num / 100;
+ else
+ delay = delay_num / delay_den;
+
+ duration = duration + delay;
}
}
- console.debug(duration);
return duration * 10;
};
export default calculateApngLength;
diff --git a/webAO/utils/getCookie.js b/webAO/utils/getCookie.ts
index 3be0733..f5b9679 100644
--- a/webAO/utils/getCookie.js
+++ b/webAO/utils/getCookie.ts
@@ -4,7 +4,7 @@
* https://www.w3schools.com/js/js_cookies.asp
* @param {String} cname The name of the cookie to return
*/
-const getCookie = (cname) => {
+const getCookie = (cname: String) => {
try {
const name = `${cname}=`;
const decodedCookie = decodeURIComponent(document.cookie);
diff --git a/webAO/utils/getResources.js b/webAO/utils/getResources.js
index a0c513e..859e63b 100644
--- a/webAO/utils/getResources.js
+++ b/webAO/utils/getResources.js
@@ -16,22 +16,22 @@ const getResources = (AO_HOST, THEME) => ({
duration: 840,
},
witnesstestimony: {
- src: `${AO_HOST}themes/${THEME}/witnesstestimony.gif`,
+ src: `${AO_HOST}themes/${THEME}/witnesstestimony_bubble.gif`,
duration: 1560,
sfx: `${AO_HOST}sounds/general/sfx-testimony.opus`,
},
crossexamination: {
- src: `${AO_HOST}themes/${THEME}/crossexamination.gif`,
+ src: `${AO_HOST}themes/${THEME}/crossexamination_bubble.gif`,
duration: 1600,
sfx: `${AO_HOST}sounds/general/sfx-testimony2.opus`,
},
guilty: {
- src: `${AO_HOST}themes/${THEME}/guilty.gif`,
+ src: `${AO_HOST}themes/${THEME}/guilty_bubble.gif`,
duration: 2870,
sfx: `${AO_HOST}sounds/general/sfx-guilty.opus`,
},
notguilty: {
- src: `${AO_HOST}themes/${THEME}/notguilty.gif`,
+ src: `${AO_HOST}themes/${THEME}/notguilty_bubble.gif`,
duration: 2440,
sfx: `${AO_HOST}sounds/general/sfx-notguilty.opus`,
},
diff --git a/webAO/utils/paths.ts b/webAO/utils/paths.ts
new file mode 100644
index 0000000..f4284b6
--- /dev/null
+++ b/webAO/utils/paths.ts
@@ -0,0 +1 @@
+export const getFilenameFromPath = (path: string) => path.substring(path.lastIndexOf('/') + 1)
diff --git a/webAO/utils/queryParser.js b/webAO/utils/queryParser.js
deleted file mode 100644
index 1c2b83a..0000000
--- a/webAO/utils/queryParser.js
+++ /dev/null
@@ -1,9 +0,0 @@
-// Get the arguments from the URL bar
-const queryParser = () => {
- const queryDict = {};
- location.search.substr(1).split('&').forEach((item) => {
- queryDict[item.split('=')[0]] = item.split('=')[1];
- });
- return queryDict;
-};
-export default queryParser;
diff --git a/webAO/utils/queryParser.ts b/webAO/utils/queryParser.ts
new file mode 100644
index 0000000..3110c14
--- /dev/null
+++ b/webAO/utils/queryParser.ts
@@ -0,0 +1,22 @@
+interface QueryParams {
+ ip: string;
+ serverIP: string;
+ mode: string;
+ asset: string;
+ theme: string;
+}
+interface StringMap {
+ [key: string]: any;
+}
+
+const queryParser = (): QueryParams => {
+ const queryDict: StringMap = {};
+ location.search
+ .substr(1)
+ .split("&")
+ .forEach((item) => {
+ queryDict[item.split("=")[0]] = item.split("=")[1];
+ });
+ return queryDict as QueryParams;
+};
+export default queryParser;
diff --git a/webAO/utils/setCookie.js b/webAO/utils/setCookie.ts
index 9734eae..084fa20 100644
--- a/webAO/utils/setCookie.js
+++ b/webAO/utils/setCookie.ts
@@ -2,9 +2,9 @@
* set a cookie
* the version from w3schools expects these to expire
* @param {String} cname The name of the cookie to return
- * @param {String} value The value of that cookie option
+ * @param {any} value The value of that cookie option
*/
-const setCookie = (cname, value) => {
+const setCookie = (cname: String, value: any) => {
document.cookie = `${cname}=${value}`;
};
export default setCookie;
diff --git a/webAO/viewport/constants/colors.ts b/webAO/viewport/constants/colors.ts
new file mode 100644
index 0000000..aad3530
--- /dev/null
+++ b/webAO/viewport/constants/colors.ts
@@ -0,0 +1,11 @@
+export const COLORS = [
+ "white",
+ "green",
+ "red",
+ "orange",
+ "blue",
+ "yellow",
+ "pink",
+ "cyan",
+ "grey",
+ ]; \ No newline at end of file
diff --git a/webAO/viewport/constants/defaultChatMsg.ts b/webAO/viewport/constants/defaultChatMsg.ts
new file mode 100644
index 0000000..8a5db6b
--- /dev/null
+++ b/webAO/viewport/constants/defaultChatMsg.ts
@@ -0,0 +1,15 @@
+import { UPDATE_INTERVAL } from "../../client";
+import { ChatMsg } from "../interfaces/ChatMsg";
+
+export const defaultChatMsg = {
+ content: "",
+ objection: 0,
+ sound: "",
+ startpreanim: true,
+ startspeaking: false,
+ side: null,
+ color: 0,
+ snddelay: 0,
+ preanimdelay: 0,
+ speed: UPDATE_INTERVAL,
+ } as ChatMsg; \ No newline at end of file
diff --git a/webAO/viewport/constants/positions.ts b/webAO/viewport/constants/positions.ts
new file mode 100644
index 0000000..1712ac6
--- /dev/null
+++ b/webAO/viewport/constants/positions.ts
@@ -0,0 +1,45 @@
+import { Positions } from '../interfaces/Positions'
+import { Desk } from '../interfaces/Desk';
+
+export const positions: Positions = {
+ def: {
+ bg: "defenseempty",
+ desk: { ao2: "defensedesk.png", ao1: "bancodefensa.png" } as Desk,
+ speedLines: "defense_speedlines.gif",
+ },
+ pro: {
+ bg: "prosecutorempty",
+ desk: { ao2: "prosecutiondesk.png", ao1: "bancoacusacion.png" } as Desk,
+ speedLines: "prosecution_speedlines.gif",
+ },
+ hld: {
+ bg: "helperstand",
+ desk: {} as Desk,
+ speedLines: "defense_speedlines.gif",
+ },
+ hlp: {
+ bg: "prohelperstand",
+ desk: {} as Desk,
+ speedLines: "prosecution_speedlines.gif",
+ },
+ wit: {
+ bg: "witnessempty",
+ desk: { ao2: "stand.png", ao1: "estrado.png" } as Desk,
+ speedLines: "prosecution_speedlines.gif",
+ },
+ jud: {
+ bg: "judgestand",
+ desk: { ao2: "judgedesk.png", ao1: "judgedesk.gif" } as Desk,
+ speedLines: "prosecution_speedlines.gif",
+ },
+ jur: {
+ bg: "jurystand",
+ desk: { ao2: "jurydesk.png", ao1: "estrado.png" } as Desk,
+ speedLines: "defense_speedlines.gif",
+ },
+ sea: {
+ bg: "seancestand",
+ desk: { ao2: "seancedesk.png", ao1: "estrado.png" } as Desk,
+ speedLines: "prosecution_speedlines.gif",
+ },
+}; \ No newline at end of file
diff --git a/webAO/viewport/constants/shouts.ts b/webAO/viewport/constants/shouts.ts
new file mode 100644
index 0000000..eddd6d3
--- /dev/null
+++ b/webAO/viewport/constants/shouts.ts
@@ -0,0 +1 @@
+export const SHOUTS = [undefined, "holdit", "objection", "takethat", "custom"];
diff --git a/webAO/viewport/interfaces/ChatMsg.ts b/webAO/viewport/interfaces/ChatMsg.ts
new file mode 100644
index 0000000..6b96c6e
--- /dev/null
+++ b/webAO/viewport/interfaces/ChatMsg.ts
@@ -0,0 +1,34 @@
+export interface ChatMsg {
+ content: string;
+ objection: number;
+ sound: string;
+ startpreanim?: boolean;
+ startspeaking?: boolean;
+ side: any;
+ color: number;
+ snddelay: number;
+ preanimdelay?: number;
+ speed: number;
+ blips: string;
+ self_offset?: number[];
+ other_offset?: number[];
+ showname?: string;
+ nameplate?: string;
+ flip?: number;
+ other_flip?: number;
+ effects?: string[];
+ deskmod?: number;
+ preanim?: string;
+ other_name?: string;
+ sprite?: string;
+ name?: string;
+ chatbox?: string;
+ other_emote?: string;
+ parsed?: HTMLSpanElement[];
+ screenshake?: number;
+ flash?: number;
+ type?: number;
+ evidence?: number;
+ looping_sfx?: boolean;
+ noninterrupting_preanim?: number;
+ } \ No newline at end of file
diff --git a/webAO/viewport/interfaces/Desk.ts b/webAO/viewport/interfaces/Desk.ts
new file mode 100644
index 0000000..872426a
--- /dev/null
+++ b/webAO/viewport/interfaces/Desk.ts
@@ -0,0 +1,4 @@
+export interface Desk {
+ ao2?: string;
+ ao1?: string;
+} \ No newline at end of file
diff --git a/webAO/viewport/interfaces/Position.ts b/webAO/viewport/interfaces/Position.ts
new file mode 100644
index 0000000..dea7238
--- /dev/null
+++ b/webAO/viewport/interfaces/Position.ts
@@ -0,0 +1,7 @@
+import { Desk } from './Desk'
+
+export interface Position {
+ bg?: string;
+ desk?: Desk;
+ speedLines: string;
+} \ No newline at end of file
diff --git a/webAO/viewport/interfaces/Positions.ts b/webAO/viewport/interfaces/Positions.ts
new file mode 100644
index 0000000..0644962
--- /dev/null
+++ b/webAO/viewport/interfaces/Positions.ts
@@ -0,0 +1,5 @@
+import { Position } from './Position'
+
+export interface Positions {
+ [key: string]: Position;
+} \ No newline at end of file
diff --git a/webAO/viewport/interfaces/Testimony.ts b/webAO/viewport/interfaces/Testimony.ts
new file mode 100644
index 0000000..61a7491
--- /dev/null
+++ b/webAO/viewport/interfaces/Testimony.ts
@@ -0,0 +1,3 @@
+export interface Testimony {
+ [key: number]: string;
+} \ No newline at end of file
diff --git a/webAO/viewport/interfaces/Viewport.ts b/webAO/viewport/interfaces/Viewport.ts
new file mode 100644
index 0000000..5b428c1
--- /dev/null
+++ b/webAO/viewport/interfaces/Viewport.ts
@@ -0,0 +1,43 @@
+import { ChatMsg } from "./ChatMsg";
+
+export interface Viewport {
+ getTextNow: Function;
+ setTextNow: Function;
+ getChatmsg: Function;
+ setChatmsg: Function;
+ getSfxPlayed: Function;
+ setSfxPlayed: Function;
+ setTickTimer: Function;
+ getTickTimer: Function;
+ getAnimating: Function;
+ setAnimating: Function;
+ getLastEvidence: Function;
+ setLastEvidence: Function;
+ setLastCharacter: Function;
+ getLastCharacter: Function;
+ setShoutTimer: Function;
+ getShoutTimer: Function;
+ setTestimonyTimer: Function;
+ getTestimonyTimer: Function;
+ setTestimonyUpdater: Function;
+ getTestimonyUpdater: Function;
+ getTheme: Function;
+ setTheme: Function;
+ testimonyAudio: HTMLAudioElement;
+ chat_tick: Function;
+ playSFX: Function;
+ set_side: Function;
+ updateTestimony: Function;
+ disposeTestimony: Function;
+ handleTextTick: Function;
+ setSfxAudio: Function;
+ getSfxAudio: Function;
+ getBackgroundFolder: Function;
+ blipChannels: HTMLAudioElement[];
+ music: any;
+ musicVolume: number;
+ setBackgroundName: Function;
+ getBackgroundName: Function;
+ shoutaudio: HTMLAudioElement;
+ updater: any;
+}
diff --git a/webAO/viewport/utils/createBlipChannels.ts b/webAO/viewport/utils/createBlipChannels.ts
new file mode 100644
index 0000000..6296b3b
--- /dev/null
+++ b/webAO/viewport/utils/createBlipChannels.ts
@@ -0,0 +1,15 @@
+import { opusCheck } from "../../dom/opusCheck";
+
+export const createBlipsChannels = () => {
+ const blipSelectors = document.getElementsByClassName(
+ "blipSound"
+ ) as HTMLCollectionOf<HTMLAudioElement>;
+
+ const blipChannels = [...blipSelectors];
+ // Allocate multiple blip audio channels to make blips less jittery
+ blipChannels.forEach((channel: HTMLAudioElement) => (channel.volume = 0.5));
+ blipChannels.forEach(
+ (channel: HTMLAudioElement) => (channel.onerror = opusCheck(channel))
+ );
+ return blipChannels;
+}; \ No newline at end of file
diff --git a/webAO/viewport/utils/createMusic.ts b/webAO/viewport/utils/createMusic.ts
new file mode 100644
index 0000000..9bf5240
--- /dev/null
+++ b/webAO/viewport/utils/createMusic.ts
@@ -0,0 +1,13 @@
+import { opusCheck } from '../../dom/opusCheck'
+
+export const createMusic = () => {
+ const audioChannels = document.getElementsByClassName(
+ "audioChannel"
+ ) as HTMLCollectionOf<HTMLAudioElement>;
+ let music = [...audioChannels];
+ music.forEach((channel: HTMLAudioElement) => (channel.volume = 0.5));
+ music.forEach(
+ (channel: HTMLAudioElement) => (channel.onerror = opusCheck(channel))
+ );
+ return music;
+}; \ No newline at end of file
diff --git a/webAO/viewport/utils/createSfxAudio.ts b/webAO/viewport/utils/createSfxAudio.ts
new file mode 100644
index 0000000..7e03563
--- /dev/null
+++ b/webAO/viewport/utils/createSfxAudio.ts
@@ -0,0 +1,9 @@
+import { AO_HOST } from "../../client/aoHost";
+
+export const createSfxAudio = () => {
+ const sfxAudio = document.getElementById(
+ "client_sfxaudio"
+ ) as HTMLAudioElement;
+ sfxAudio.src = `${AO_HOST}sounds/general/sfx-realization.opus`;
+ return sfxAudio;
+}; \ No newline at end of file
diff --git a/webAO/viewport/utils/createShoutAudio.ts b/webAO/viewport/utils/createShoutAudio.ts
new file mode 100644
index 0000000..8211116
--- /dev/null
+++ b/webAO/viewport/utils/createShoutAudio.ts
@@ -0,0 +1,9 @@
+import { AO_HOST } from "../../client/aoHost";
+
+export const createShoutAudio = () => {
+ const shoutAudio = document.getElementById(
+ "client_shoutaudio"
+ ) as HTMLAudioElement;
+ shoutAudio.src = `${AO_HOST}misc/default/objection.opus`;
+ return shoutAudio;
+}; \ No newline at end of file
diff --git a/webAO/viewport/utils/createTestimonyAudio.ts b/webAO/viewport/utils/createTestimonyAudio.ts
new file mode 100644
index 0000000..2ff98f6
--- /dev/null
+++ b/webAO/viewport/utils/createTestimonyAudio.ts
@@ -0,0 +1,9 @@
+import { AO_HOST } from '../../client/aoHost'
+
+export const createTestimonyAudio = () => {
+ const testimonyAudio = document.getElementById(
+ "client_testimonyaudio"
+ ) as HTMLAudioElement;
+ testimonyAudio.src = `${AO_HOST}sounds/general/sfx-guilty.opus`;
+ return testimonyAudio;
+}; \ No newline at end of file
diff --git a/webAO/viewport/utils/handleICSpeaking.ts b/webAO/viewport/utils/handleICSpeaking.ts
new file mode 100644
index 0000000..c396093
--- /dev/null
+++ b/webAO/viewport/utils/handleICSpeaking.ts
@@ -0,0 +1,312 @@
+import { ChatMsg } from "../interfaces/ChatMsg";
+import { client } from "../../client";
+import { appendICLog } from "../../client/appendICLog";
+import { checkCallword } from "../../client/checkCallword";
+import setEmote from "../../client/setEmote";
+import { AO_HOST } from "../../client/aoHost";
+import { SHOUTS } from "../constants/shouts";
+import getAnimLength from "../../utils/getAnimLength";
+import { setChatbox } from "../../dom/setChatbox";
+import { resizeChatbox } from "../../dom/resizeChatbox";
+import transparentPng from "../../constants/transparentPng";
+import { COLORS } from "../constants/colors";
+import mlConfig from "../../utils/aoml";
+
+const attorneyMarkdown = mlConfig(AO_HOST);
+
+export let startFirstTickCheck: boolean;
+export const setStartFirstTickCheck = (val: boolean) => {startFirstTickCheck = val}
+export let startSecondTickCheck: boolean;
+export const setStartSecondTickCheck = (val: boolean) => {startSecondTickCheck = val}
+export let startThirdTickCheck: boolean;
+export const setStartThirdTickCheck = (val: boolean) => {startThirdTickCheck = val}
+/**
+ * Sets a new emote.
+ * This sets up everything before the tick() loops starts
+ * a lot of things can probably be moved here, like starting the shout animation if there is one
+ * TODO: the preanim logic, on the other hand, should probably be moved to tick()
+ * @param {object} chatmsg the new chat message
+ */
+export const handle_ic_speaking = async (playerChatMsg: ChatMsg) => {
+ client.viewport.setChatmsg(playerChatMsg);
+ client.viewport.setTextNow("");
+ client.viewport.setSfxPlayed(0);
+ client.viewport.setTickTimer(0);
+ client.viewport.setAnimating(true);
+
+ startFirstTickCheck = true;
+ startSecondTickCheck = false;
+ startThirdTickCheck = false;
+ let charLayers = document.getElementById("client_char")!;
+ let pairLayers = document.getElementById("client_pair_char")!;
+ // stop updater
+ clearTimeout(client.viewport.updater);
+
+ // stop last sfx from looping any longer
+ client.viewport.getSfxAudio().loop = false;
+
+ const fg = <HTMLImageElement>document.getElementById("client_fg");
+ const gamewindow = document.getElementById("client_gamewindow")!;
+ const waitingBox = document.getElementById("client_chatwaiting")!;
+
+ // Reset CSS animation
+ gamewindow.style.animation = "";
+ waitingBox.style.opacity = "0";
+
+ const eviBox = document.getElementById("client_evi")!;
+
+ if (client.viewport.getLastEvidence() !== client.viewport.getChatmsg().evidence) {
+ eviBox.style.opacity = "0";
+ eviBox.style.height = "0%";
+ }
+ client.viewport.setLastEvidence(client.viewport.getChatmsg().evidence);
+
+ const validSides: string[] = ["def", "pro", "wit"]; // these are for the full view pan, the other positions use 'client_char'
+ if (validSides.includes(client.viewport.getChatmsg().side)) {
+ charLayers = document.getElementById(`client_${client.viewport.getChatmsg().side}_char`);
+ pairLayers = document.getElementById(`client_${client.viewport.getChatmsg().side}_pair_char`);
+ }
+
+ const chatContainerBox = document.getElementById("client_chatcontainer")!;
+ const nameBoxInner = document.getElementById("client_inner_name")!;
+ const chatBoxInner = document.getElementById("client_inner_chat")!;
+
+ const displayname =
+ (<HTMLInputElement>document.getElementById("showname")).checked &&
+ client.viewport.getChatmsg().showname !== ""
+ ? client.viewport.getChatmsg().showname!
+ : client.viewport.getChatmsg().nameplate!;
+
+ // Clear out the last message
+ chatBoxInner.innerText = client.viewport.getTextNow();
+ nameBoxInner.innerText = displayname;
+
+ if (client.viewport.getLastCharacter() !== client.viewport.getChatmsg().name) {
+ charLayers.style.opacity = "0";
+ pairLayers.style.opacity = "0";
+ }
+
+ client.viewport.setLastCharacter(client.viewport.getChatmsg().name);
+
+ appendICLog(client.viewport.getChatmsg().content, client.viewport.getChatmsg().showname, client.viewport.getChatmsg().nameplate);
+
+ checkCallword(client.viewport.getChatmsg().content, client.viewport.getSfxAudio());
+
+ setEmote(
+ AO_HOST,
+ client,
+ client.viewport.getChatmsg().name!.toLowerCase(),
+ client.viewport.getChatmsg().sprite!,
+ "(a)",
+ false,
+ client.viewport.getChatmsg().side
+ );
+
+ if (client.viewport.getChatmsg().other_name) {
+ setEmote(
+ AO_HOST,
+ client,
+ client.viewport.getChatmsg().other_name.toLowerCase(),
+ client.viewport.getChatmsg().other_emote!,
+ "(a)",
+ false,
+ client.viewport.getChatmsg().side
+ );
+ }
+
+ // gets which shout shall played
+ const shoutSprite = <HTMLImageElement>(
+ document.getElementById("client_shout")
+ );
+
+ const shout = SHOUTS[client.viewport.getChatmsg().objection];
+ if (shout) {
+ // Hide message box
+ chatContainerBox.style.opacity = "0";
+ if (client.viewport.getChatmsg().objection === 4) {
+ shoutSprite.src = `${AO_HOST}characters/${encodeURI(
+ client.viewport.getChatmsg().name!.toLowerCase()
+ )}/custom.gif`;
+ } else {
+ shoutSprite.src = client.resources[shout].src;
+ shoutSprite.style.animation = "bubble 700ms steps(10, jump-both)";
+ }
+ shoutSprite.style.opacity = "1";
+
+ client.viewport.shoutaudio.src = `${AO_HOST}characters/${encodeURI(
+ client.viewport.getChatmsg().name.toLowerCase()
+ )}/${shout}.opus`;
+ client.viewport.shoutaudio.play();
+ client.viewport.setShoutTimer(client.resources[shout].duration);
+ } else {
+ client.viewport.setShoutTimer(0);
+ }
+
+ client.viewport.getChatmsg().startpreanim = true;
+ let gifLength = 0;
+
+ if (client.viewport.getChatmsg().type === 1 && client.viewport.getChatmsg().preanim !== "-") {
+ //we have a preanim
+ chatContainerBox.style.opacity = "0";
+
+ gifLength = await getAnimLength(
+ `${AO_HOST}characters/${encodeURI(
+ client.viewport.getChatmsg().name!.toLowerCase()
+ )}/${encodeURI(client.viewport.getChatmsg().preanim)}`
+ );
+ console.debug("preanim is " + gifLength + " long");
+ client.viewport.getChatmsg().startspeaking = false;
+ } else {
+ client.viewport.getChatmsg().startspeaking = true;
+ if (client.viewport.getChatmsg().content !== "") chatContainerBox.style.opacity = "1";
+ }
+ client.viewport.getChatmsg().preanimdelay = gifLength;
+ const setAside = {
+ position: client.viewport.getChatmsg().side,
+ showSpeedLines: false,
+ showDesk: false,
+ };
+ let skipoffset: boolean = false;
+ if (client.viewport.getChatmsg().type === 5) {
+ setAside.showSpeedLines = true;
+ setAside.showDesk = false;
+ client.viewport.set_side(setAside);
+ } else {
+ switch (Number(client.viewport.getChatmsg().deskmod)) {
+ case 0: //desk is hidden
+ setAside.showSpeedLines = false;
+ setAside.showDesk = false;
+ client.viewport.set_side(setAside);
+ break;
+ case 1: //desk is shown
+ setAside.showSpeedLines = false;
+ setAside.showDesk = true;
+ client.viewport.set_side(setAside);
+ break;
+ case 2: //desk is hidden during preanim, but shown during idle/talk
+ setAside.showSpeedLines = false;
+ setAside.showDesk = false;
+ client.viewport.set_side(setAside);
+ break;
+ case 3: //opposite of 2
+ setAside.showSpeedLines = false;
+ setAside.showDesk = false;
+ client.viewport.set_side(setAside);
+ break;
+ case 4: //desk is hidden, character offset is ignored, pair character is hidden during preanim, normal behavior during idle/talk
+ setAside.showSpeedLines = false;
+ setAside.showDesk = false;
+ client.viewport.set_side(setAside);
+ skipoffset = true;
+ break;
+ case 5: //opposite of 4
+ setAside.showSpeedLines = false;
+ setAside.showDesk = true;
+ client.viewport.set_side(setAside);
+ break;
+ default:
+ setAside.showSpeedLines = false;
+ setAside.showDesk = true;
+ client.viewport.set_side(setAside);
+ break;
+ }
+ }
+
+ setChatbox(client.viewport.getChatmsg().chatbox);
+ resizeChatbox();
+
+ if (!skipoffset) {
+ // Flip the character
+ charLayers.style.transform =
+ client.viewport.getChatmsg().flip === 1 ? "scaleX(-1)" : "scaleX(1)";
+ pairLayers.style.transform =
+ client.viewport.getChatmsg().other_flip === 1 ? "scaleX(-1)" : "scaleX(1)";
+
+ // Shift by the horizontal offset
+ switch (client.viewport.getChatmsg().side) {
+ case "wit":
+ pairLayers.style.left = `${200 + Number(client.viewport.getChatmsg().other_offset[0])}%`;
+ charLayers.style.left = `${200 + Number(client.viewport.getChatmsg().self_offset[0])}%`;
+ break;
+ case "pro":
+ pairLayers.style.left = `${400 + Number(client.viewport.getChatmsg().other_offset[0])}%`;
+ charLayers.style.left = `${400 + Number(client.viewport.getChatmsg().self_offset[0])}%`;
+ break;
+ default:
+ pairLayers.style.left = `${Number(client.viewport.getChatmsg().other_offset[0])}%`;
+ charLayers.style.left = `${Number(client.viewport.getChatmsg().self_offset[0])}%`;
+ break;
+ }
+
+ // New vertical offsets
+ pairLayers.style.top = `${Number(client.viewport.getChatmsg().other_offset[1])}%`;
+ charLayers.style.top = `${Number(client.viewport.getChatmsg().self_offset[1])}%`;
+ }
+
+ client.viewport.blipChannels.forEach(
+ (channel: HTMLAudioElement) =>
+ (channel.src = `${AO_HOST}sounds/general/sfx-blip${encodeURI(
+ client.viewport.getChatmsg().blips.toLowerCase()
+ )}.opus`)
+ );
+
+ // process markup
+ if (client.viewport.getChatmsg().content.startsWith("~~")) {
+ chatBoxInner.style.textAlign = "center";
+ client.viewport.getChatmsg().content = client.viewport.getChatmsg().content.substring(2, client.viewport.getChatmsg().content.length);
+ } else {
+ chatBoxInner.style.textAlign = "inherit";
+ }
+
+ // apply effects
+ fg.style.animation = "";
+ const effectName = client.viewport.getChatmsg().effects[0].toLowerCase();
+ const badEffects = ["", "-", "none"];
+ if (effectName.startsWith("rain")) {
+ (<HTMLLinkElement>document.getElementById("effect_css")).href = "styles/effects/rain.css";
+ let intensity = 200;
+ if (effectName.endsWith("weak")) {
+ intensity = 100;
+ } else if (effectName.endsWith("strong")) {
+ intensity = 400;
+ }
+ if (intensity < fg.childElementCount)
+ fg.innerHTML = '';
+ else
+ intensity = intensity - fg.childElementCount;
+
+ for (let i = 0; i < intensity; i++) {
+ let drop = document.createElement("p");
+ drop.style.left = (Math.random() * 100) + "%";
+ drop.style.animationDelay = String(Math.random()) + "s";
+ fg.appendChild(drop)
+ }
+ } else if (
+ client.viewport.getChatmsg().effects[0] &&
+ !badEffects.includes(effectName)
+ ) {
+ (<HTMLLinkElement>document.getElementById("effect_css")).href = "";
+ fg.innerHTML = '';
+ const baseEffectUrl = `${AO_HOST}themes/default/effects/`;
+ fg.src = `${baseEffectUrl}${encodeURI(effectName)}.webp`;
+ } else {
+ fg.innerHTML = '';
+ fg.src = transparentPng;
+ }
+
+
+ charLayers.style.opacity = "1";
+
+ const soundChecks = ["0", "1", "", undefined];
+ if (soundChecks.some((check) => client.viewport.getChatmsg().sound === check)) {
+ client.viewport.getChatmsg().sound = client.viewport.getChatmsg().effects[2];
+ }
+
+ client.viewport.getChatmsg().parsed = await attorneyMarkdown.applyMarkdown(
+ client.viewport.getChatmsg().content,
+
+ COLORS[client.viewport.getChatmsg().color]
+
+ );
+ client.viewport.chat_tick();
+}; \ No newline at end of file
diff --git a/webAO/viewport/utils/initTestimonyUpdater.ts b/webAO/viewport/utils/initTestimonyUpdater.ts
new file mode 100644
index 0000000..e6f6e9d
--- /dev/null
+++ b/webAO/viewport/utils/initTestimonyUpdater.ts
@@ -0,0 +1,31 @@
+import { Testimony } from '../interfaces/Testimony'
+import { client, UPDATE_INTERVAL } from '../../client'
+/**
+ * Intialize testimony updater
+ */
+export const initTestimonyUpdater = () => {
+ const testimonyFilenames: Testimony = {
+ 1: "witnesstestimony",
+ 2: "crossexamination",
+ 3: "notguilty",
+ 4: "guilty",
+ };
+
+ const testimony = testimonyFilenames[client.testimonyID];
+ if (!testimony) {
+ console.warn(`Invalid testimony ID ${client.testimonyID}`);
+ return;
+ }
+
+ client.viewport.testimonyAudio.src = client.resources[testimony].sfx;
+ client.viewport.testimonyAudio.play();
+
+ const testimonyOverlay = <HTMLImageElement>(
+ document.getElementById("client_testimony")
+ );
+ testimonyOverlay.src = client.resources[testimony].src;
+ testimonyOverlay.style.opacity = "1";
+
+ client.viewport.setTestimonyTimer(0);
+ client.viewport.setTestimonyUpdater(setTimeout(() => client.viewport.updateTestimony(), UPDATE_INTERVAL));
+}; \ No newline at end of file
diff --git a/webAO/viewport/utils/setSide.ts b/webAO/viewport/utils/setSide.ts
new file mode 100644
index 0000000..15cb7c6
--- /dev/null
+++ b/webAO/viewport/utils/setSide.ts
@@ -0,0 +1,91 @@
+import { positions } from '../constants/positions'
+import { AO_HOST } from '../../client/aoHost'
+import { client } from '../../client'
+import tryUrls from '../../utils/tryUrls';
+import fileExists from '../../utils/fileExists';
+
+/**
+ * Changes the viewport background based on a given position.
+ *
+ * Valid positions: `def, pro, hld, hlp, wit, jud, jur, sea`
+ * @param {string} position the position to change into
+ */
+export const set_side = async ({
+ position,
+ showSpeedLines,
+ showDesk,
+}: {
+ position: string;
+ showSpeedLines: boolean;
+ showDesk: boolean;
+}) => {
+ const view = document.getElementById("client_fullview")!;
+ console.log(position)
+ let bench: HTMLImageElement;
+ if (['def','pro','wit'].includes(position)) {
+ bench = <HTMLImageElement>(
+ document.getElementById(`client_${position}_bench`)
+ );
+ } else {
+ bench = <HTMLImageElement>document.getElementById("client_bench_classic");
+ }
+
+ let court: HTMLImageElement;
+ if ("def,pro,wit".includes(position)) {
+ court = <HTMLImageElement>(
+ document.getElementById(`client_court_${position}`)
+ );
+ } else {
+ court = <HTMLImageElement>document.getElementById("client_court_classic");
+ }
+
+ let bg;
+ let desk;
+ let speedLines;
+
+ if ("def,pro,hld,hlp,wit,jud,jur,sea".includes(position)) {
+ bg = positions[position].bg;
+ desk = positions[position].desk;
+ speedLines = positions[position].speedLines;
+ } else {
+ bg = `${position}`;
+ desk = { ao2: `${position}_overlay.png`, ao1: "_overlay.png" };
+ speedLines = "defense_speedlines.gif";
+ }
+
+ if (showSpeedLines === true) {
+ court.src = `${AO_HOST}themes/default/${encodeURI(speedLines)}`;
+ } else {
+ court.src = await tryUrls(client.viewport.getBackgroundFolder() + bg);
+ }
+
+
+ if (showDesk === true && desk) {
+ const deskFilename = (await fileExists(client.viewport.getBackgroundFolder() + desk.ao2))
+ ? desk.ao2
+ : desk.ao1;
+ bench.src = client.viewport.getBackgroundFolder() + deskFilename;
+ bench.style.opacity = "1";
+ } else {
+ bench.style.opacity = "0";
+ }
+
+ if ("def,pro,wit".includes(position)) {
+ view.style.display = "";
+ document.getElementById("client_classicview")!.style.display = "none";
+ switch (position) {
+ case "def":
+ view.style.left = "0";
+ break;
+ case "wit":
+ view.style.left = "-200%";
+ break;
+ case "pro":
+ view.style.left = "-400%";
+ break;
+ }
+ } else {
+ view.style.display = "none";
+ document.getElementById("client_classicview").style.display = "";
+ }
+};
diff --git a/webAO/viewport/viewport.ts b/webAO/viewport/viewport.ts
new file mode 100644
index 0000000..9ac6e96
--- /dev/null
+++ b/webAO/viewport/viewport.ts
@@ -0,0 +1,498 @@
+import { client, delay } from "../client";
+import { UPDATE_INTERVAL } from "../client";
+import setEmote from "../client/setEmote";
+import { safeTags } from "../encoding";
+import { AO_HOST } from "../client/aoHost";
+import { Viewport } from './interfaces/Viewport'
+import { createBlipsChannels } from './utils/createBlipChannels'
+import { defaultChatMsg } from './constants/defaultChatMsg'
+import { createMusic } from './utils/createMusic'
+import { createSfxAudio } from './utils/createSfxAudio'
+import { createShoutAudio } from './utils/createShoutAudio'
+import { createTestimonyAudio } from './utils/createTestimonyAudio'
+import { Testimony } from './interfaces/Testimony'
+import { COLORS } from './constants/colors'
+import { set_side } from './utils/setSide'
+import { ChatMsg } from "./interfaces/ChatMsg";
+import { setStartFirstTickCheck, setStartSecondTickCheck, startFirstTickCheck, startSecondTickCheck } from "./utils/handleICSpeaking";
+
+const viewport = (): Viewport => {
+ let animating = false;
+ let blipChannels = createBlipsChannels();
+ let chatmsg = defaultChatMsg;
+ let currentBlipChannel = 0;
+ let lastChar = "";
+ let lastEvi = 0;
+ let music = createMusic();
+ let musicVolume = 0;
+ let sfxAudio = createSfxAudio();
+ let sfxplayed = 0;
+ let shoutTimer = 0;
+ let shoutaudio = createShoutAudio();
+ let testimonyAudio = createTestimonyAudio();
+ let testimonyTimer = 0;
+ let testimonyUpdater: any;
+ let textnow = "";
+ let theme: string;
+ let tickTimer = 0;
+ let updater: any;
+ let backgroundName = "";
+ const getSfxAudio = () => sfxAudio;
+ const setSfxAudio = (value: HTMLAudioElement) => { sfxAudio = value };
+ const getBackgroundName = () => backgroundName;
+ const setBackgroundName = (value: string) => { backgroundName = value };
+ const getBackgroundFolder = () =>
+ `${AO_HOST}background/${encodeURI(backgroundName.toLowerCase())}/`;
+ const getTextNow = () => {return textnow}
+ const setTextNow = (val: string) => {textnow = val}
+ const getChatmsg = () => {return chatmsg}
+ const setChatmsg = (val: ChatMsg) => {chatmsg = val}
+ const getSfxPlayed = () => sfxplayed
+ const setSfxPlayed = (val: number) => {sfxplayed = val}
+ const getTickTimer = () => tickTimer
+ const setTickTimer = (val: number) => {tickTimer = val}
+ const getAnimating = () => animating
+ const setAnimating = (val: boolean) => {animating = val}
+ const getLastEvidence = () => lastEvi
+ const setLastEvidence = (val: number) => {lastEvi = val}
+ const setLastCharacter = (val: string) => {lastChar = val}
+ const getLastCharacter = () => lastChar
+ const getShoutTimer = () => shoutTimer
+ const setShoutTimer = (val: number) => {shoutTimer = val}
+ const getTheme = () => theme
+ const setTheme = (val: string) => {theme = val}
+ const getTestimonyTimer = () => testimonyTimer;
+ const setTestimonyTimer = (val: number) => {testimonyTimer = val}
+ const setTestimonyUpdater = (val: any) => {testimonyUpdater = val}
+ const getTestimonyUpdater = () => testimonyUpdater
+ const playSFX = async (sfxname: string, looping: boolean) => {
+ sfxAudio.pause();
+ sfxAudio.loop = looping;
+ sfxAudio.src = sfxname;
+ sfxAudio.play();
+ };
+ /**
+ * Updates the testimony overaly
+ */
+ const updateTestimony = () => {
+ const testimonyFilenames: Testimony = {
+ 1: "witnesstestimony",
+ 2: "crossexamination",
+ 3: "notguilty",
+ 4: "guilty",
+ };
+
+ // Update timer
+ testimonyTimer += UPDATE_INTERVAL;
+
+ const testimony = testimonyFilenames[client.testimonyID];
+ const resource = client.resources[testimony];
+ if (!resource) {
+ disposeTestimony();
+ return;
+ }
+
+ if (testimonyTimer >= resource.duration) {
+ disposeTestimony();
+ } else {
+ testimonyUpdater = setTimeout(() => updateTestimony(), UPDATE_INTERVAL);
+ }
+ };
+ /**
+ * Dispose the testimony overlay
+ */
+ const disposeTestimony = () => {
+ client.testimonyID = 0;
+ testimonyTimer = 0;
+ document.getElementById("client_testimony").style.opacity = "0";
+ clearTimeout(testimonyUpdater);
+ };
+ const handleTextTick = async (charLayers: HTMLImageElement) => {
+ const chatBox = document.getElementById("client_chat");
+ const waitingBox = document.getElementById("client_chatwaiting");
+ const chatBoxInner = document.getElementById("client_inner_chat");
+ const charName = chatmsg.name.toLowerCase();
+ const charEmote = chatmsg.sprite.toLowerCase();
+
+ if (chatmsg.content.charAt(textnow.length) !== " ") {
+ blipChannels[currentBlipChannel].play();
+ currentBlipChannel++;
+ currentBlipChannel %= blipChannels.length;
+ }
+ textnow = chatmsg.content.substring(0, textnow.length + 1);
+ const characterElement = chatmsg.parsed[textnow.length - 1];
+ if (characterElement) {
+ const COMMAND_IDENTIFIER = "\\";
+
+ const nextCharacterElement = chatmsg.parsed[textnow.length];
+ const flash = async () => {
+ const effectlayer = document.getElementById("client_fg");
+ playSFX(`${AO_HOST}sounds/general/sfx-realization.opus`, false);
+ effectlayer.style.animation = "flash 0.4s 1";
+ await delay(400);
+ effectlayer.style.removeProperty("animation");
+ };
+
+ const shake = async () => {
+ const gamewindow = document.getElementById("client_gamewindow");
+ playSFX(`${AO_HOST}sounds/general/sfx-stab.opus`, false);
+ gamewindow.style.animation = "shake 0.2s 1";
+ await delay(200);
+ gamewindow.style.removeProperty("animation");
+ };
+
+ const commands = new Map(
+ Object.entries({
+ s: shake,
+ f: flash,
+ })
+ );
+ const textSpeeds = new Set(["{", "}"]);
+
+ // Changing Text Speed
+ if (textSpeeds.has(characterElement.innerHTML)) {
+ // Grab them all in a row
+ const MAX_SLOW_CHATSPEED = 120;
+ for (let i = textnow.length; i < chatmsg.content.length; i++) {
+ const currentCharacter = chatmsg.parsed[i - 1].innerHTML;
+ if (currentCharacter === "}") {
+ if (chatmsg.speed > 0) {
+ chatmsg.speed -= 20;
+ }
+ } else if (currentCharacter === "{") {
+ if (chatmsg.speed < MAX_SLOW_CHATSPEED) {
+ chatmsg.speed += 20;
+ }
+ } else {
+ // No longer at a speed character
+ textnow = chatmsg.content.substring(0, i);
+ break;
+ }
+ }
+ }
+
+ if (
+ characterElement.innerHTML === COMMAND_IDENTIFIER &&
+ commands.has(nextCharacterElement?.innerHTML)
+ ) {
+ textnow = chatmsg.content.substring(0, textnow.length + 1);
+ await commands.get(nextCharacterElement.innerHTML)();
+ } else {
+ chatBoxInner.appendChild(chatmsg.parsed[textnow.length - 1]);
+ }
+ }
+ // scroll to bottom
+ chatBox.scrollTop = chatBox.scrollHeight;
+
+ if (textnow === chatmsg.content) {
+ animating = false;
+ setEmote(
+ AO_HOST,
+ client,
+ charName,
+ charEmote,
+ "(a)",
+ false,
+ chatmsg.side
+ );
+ charLayers.style.opacity = "1";
+ waitingBox.style.opacity = "1";
+ clearTimeout(updater);
+ }
+ };
+ /**
+ * Updates the chatbox based on the given text.
+ *
+ * OK, here's the documentation on how this works:
+ *
+ * 1 _animating
+ * If we're not done with this characters animation, i.e. his text isn't fully there, set a timeout for the next tick/step to happen
+ *
+ * 2 startpreanim
+ * If the shout timer is over it starts with the preanim
+ * The first thing it checks for is the shake effect (TODO on client this is handled by the @ symbol and not a flag )
+ * Then is the flash/realization effect
+ * After that, the shout image set to be transparent
+ * and the main characters preanim gif is loaded
+ * If pairing is supported the paired character will just stand around with his idle sprite
+ *
+ * 3 preanimdelay over
+ * this animates the evidence popup and finally shows the character name and message box
+ * it sets the text color and the character speaking sprite
+ *
+ * 4 textnow != content
+ * this adds a character to the textbox and stops the animations if the entire message is present in the textbox
+ *
+ * 5 sfx
+ * independent of the stuff above, this will play any sound effects specified by the emote the character sent.
+ * happens after the shout delay + an sfx delay that comes with the message packet
+ *
+ * XXX: This relies on a global variable `chatmsg`!
+ */
+ const chat_tick = async () => {
+ // note: this is called fairly often
+ // do not perform heavy operations here
+ console.log(textnow)
+ console.log(chatmsg.content)
+ await delay(chatmsg.speed);
+ if (textnow === chatmsg.content) {
+ return;
+ }
+
+ const gamewindow = document.getElementById("client_gamewindow");
+ const waitingBox = document.getElementById("client_chatwaiting");
+ const eviBox = <HTMLImageElement>document.getElementById("client_evi");
+ const shoutSprite = <HTMLImageElement>(
+ document.getElementById("client_shout")
+ );
+ const effectlayer = <HTMLImageElement>document.getElementById("client_fg");
+ const chatBoxInner = document.getElementById("client_inner_chat");
+ let charLayers = <HTMLImageElement>document.getElementById("client_char");
+ let pairLayers = <HTMLImageElement>(
+ document.getElementById("client_pair_char")
+ );
+
+ const validSides: string[] = ["def", "pro", "wit"]; // these are for the full view pan, the other positions use 'client_char'
+ if (validSides.includes(chatmsg.side)) {
+ charLayers = <HTMLImageElement>(
+ document.getElementById(`client_${chatmsg.side}_char`)
+ );
+ pairLayers = <HTMLImageElement>(
+ document.getElementById(`client_${chatmsg.side}_pair_char`)
+ );
+ }
+
+ const charName = chatmsg.name.toLowerCase();
+ const charEmote = chatmsg.sprite.toLowerCase();
+
+ const pairName = chatmsg.other_name.toLowerCase();
+ const pairEmote = chatmsg.other_emote.toLowerCase();
+
+ // TODO: preanims sometimes play when they're not supposed to
+ const isShoutOver = tickTimer >= shoutTimer;
+ const isShoutAndPreanimOver =
+ tickTimer >= shoutTimer + chatmsg.preanimdelay;
+ if (isShoutOver && startFirstTickCheck) {
+ // Effect stuff
+ if (chatmsg.screenshake === 1) {
+ // Shake screen
+ playSFX(`${AO_HOST}sounds/general/sfx-stab.opus`, false);
+ gamewindow.style.animation = "shake 0.2s 1";
+ }
+ if (chatmsg.flash === 1) {
+ // Flash screen
+ playSFX(`${AO_HOST}sounds/general/sfx-realization.opus`, false);
+ effectlayer.style.animation = "flash 0.4s 1";
+ }
+
+ // Pre-animation stuff
+ if (chatmsg.preanimdelay > 0) {
+ shoutSprite.style.opacity = "0";
+ shoutSprite.style.animation = "";
+ const preanim = chatmsg.preanim.toLowerCase();
+ setEmote(AO_HOST, client, charName, preanim, "", false, chatmsg.side);
+ }
+
+ if (chatmsg.other_name) {
+ pairLayers.style.opacity = "1";
+ } else {
+ pairLayers.style.opacity = "0";
+ }
+
+ // Done with first check, move to second
+ setStartFirstTickCheck(false)
+ setStartSecondTickCheck(true)
+
+ chatmsg.startpreanim = false;
+ chatmsg.startspeaking = true;
+ }
+
+ const hasNonInterruptingPreAnim = chatmsg.noninterrupting_preanim === 1;
+ if (textnow !== chatmsg.content && hasNonInterruptingPreAnim) {
+ const chatContainerBox = document.getElementById("client_chatcontainer");
+ chatContainerBox.style.opacity = "1";
+ await handleTextTick(charLayers);
+ } else if (isShoutAndPreanimOver && startSecondTickCheck) {
+ if (chatmsg.startspeaking) {
+ chatmsg.startspeaking = false;
+
+ // Evidence Bullshit
+ if (chatmsg.evidence > 0) {
+ // Prepare evidence
+ eviBox.src = safeTags(
+ client.evidences[chatmsg.evidence - 1].icon
+ );
+
+ eviBox.style.width = "auto";
+ eviBox.style.height = "36.5%";
+ eviBox.style.opacity = "1";
+
+ testimonyAudio.src = `${AO_HOST}sounds/general/sfx-evidenceshoop.opus`;
+ testimonyAudio.play();
+
+ if (chatmsg.side === "def") {
+ // Only def show evidence on right
+ eviBox.style.right = "1em";
+ eviBox.style.left = "initial";
+ } else {
+ eviBox.style.right = "initial";
+ eviBox.style.left = "1em";
+ }
+ }
+ chatBoxInner.className = `text_${COLORS[chatmsg.color]}`;
+
+ if (chatmsg.preanimdelay === 0) {
+ shoutSprite.style.opacity = "0";
+ shoutSprite.style.animation = "";
+ }
+
+ switch (Number(chatmsg.deskmod)) {
+ case 2:
+ set_side({
+ position: chatmsg.side,
+ showSpeedLines: false,
+ showDesk: true,
+ });
+ break;
+ case 3:
+ set_side({
+ position: chatmsg.side,
+ showSpeedLines: false,
+ showDesk: false,
+ });
+ break;
+ case 4:
+ set_side({
+ position: chatmsg.side,
+ showSpeedLines: false,
+ showDesk: true,
+ });
+ break;
+ case 5:
+ set_side({
+ position: chatmsg.side,
+ showSpeedLines: false,
+ showDesk: false,
+ });
+ break;
+ }
+
+ if (chatmsg.other_name) {
+ setEmote(
+ AO_HOST,
+ client,
+ pairName,
+ pairEmote,
+ "(a)",
+ true,
+ chatmsg.side
+ );
+ pairLayers.style.opacity = "1";
+ } else {
+ pairLayers.style.opacity = "0";
+ }
+
+ setEmote(
+ AO_HOST,
+ client,
+ charName,
+ charEmote,
+ "(b)",
+ false,
+ chatmsg.side
+ );
+ charLayers.style.opacity = "1";
+
+ if (textnow === chatmsg.content) {
+ setEmote(
+ AO_HOST,
+ client,
+ charName,
+ charEmote,
+ "(a)",
+ false,
+ chatmsg.side
+ );
+ charLayers.style.opacity = "1";
+ waitingBox.style.opacity = "1";
+ animating = false;
+ clearTimeout(updater);
+ return;
+ }
+ } else if (textnow !== chatmsg.content) {
+ const chatContainerBox = document.getElementById(
+ "client_chatcontainer"
+ );
+ chatContainerBox.style.opacity = "1";
+ await handleTextTick(charLayers);
+ }
+ }
+
+ if (!sfxplayed && chatmsg.snddelay + shoutTimer >= tickTimer) {
+ sfxplayed = 1;
+ if (
+ chatmsg.sound !== "0" &&
+ chatmsg.sound !== "1" &&
+ chatmsg.sound !== "" &&
+ chatmsg.sound !== undefined &&
+ (chatmsg.type == 1 || chatmsg.type == 2 || chatmsg.type == 6)
+ ) {
+ playSFX(
+ `${AO_HOST}sounds/general/${encodeURI(
+ chatmsg.sound.toLowerCase()
+ )}.opus`,
+ chatmsg.looping_sfx
+ );
+ }
+ }
+ console.log(animating)
+ if (animating) {
+ chat_tick();
+ }
+ tickTimer += UPDATE_INTERVAL;
+ };
+
+ return {
+ getTextNow,
+ setTextNow,
+ getChatmsg,
+ setChatmsg,
+ getSfxPlayed,
+ setSfxPlayed,
+ setTickTimer,
+ getTickTimer,
+ setAnimating,
+ getAnimating,
+ getLastEvidence,
+ setLastEvidence,
+ setLastCharacter,
+ getLastCharacter,
+ getShoutTimer,
+ setShoutTimer,
+ setTheme,
+ getTheme,
+ setTestimonyTimer,
+ getTestimonyTimer,
+ setTestimonyUpdater,
+ getTestimonyUpdater,
+ testimonyAudio,
+ chat_tick,
+ playSFX,
+ set_side,
+ setBackgroundName,
+ updateTestimony,
+ disposeTestimony,
+ handleTextTick,
+ getBackgroundFolder,
+ getBackgroundName,
+ getSfxAudio,
+ setSfxAudio,
+ blipChannels,
+ music,
+ musicVolume,
+ shoutaudio,
+ updater,
+ };
+};
+
+export default viewport;