aboutsummaryrefslogtreecommitdiff
path: root/webAO/client.js
diff options
context:
space:
mode:
Diffstat (limited to 'webAO/client.js')
-rw-r--r--webAO/client.js1591
1 files changed, 1296 insertions, 295 deletions
diff --git a/webAO/client.js b/webAO/client.js
index d6d388e..c690e1f 100644
--- a/webAO/client.js
+++ b/webAO/client.js
@@ -1,12 +1,23 @@
/*
* Glorious webAO
- * made by sD, refactored by oldmud0
+ * made by sD, refactored by oldmud0 and Qubrick
* credits to aleks for original idea and source
*/
-let queryDict = {};
-location.search.substr(1).split("&").forEach(function(item) {
- queryDict[item.split("=")[0]] = item.split("=")[1]
+// Uses the Gify library:
+// https://github.com/rfrench/gify
+// The following comment is needed for ESLint:
+/* global gify */
+
+import background_arr from "./backgrounds.js";
+import evidence_arr from "./evidence.js";
+import Fingerprint from "./fingerprint.js";
+
+import { EventEmitter } from "events";
+
+const queryDict = {};
+location.search.substr(1).split("&").forEach(function (item) {
+ queryDict[item.split("=")[0]] = item.split("=")[1];
});
/* Server magic */
@@ -16,64 +27,119 @@ let mode = queryDict.mode;
const AO_HOST = queryDict.asset || "http://s3.wasabisys.com/webao/base/";
const MUSIC_HOST = AO_HOST + "sounds/music/";
-const BAR_WIDTH = 90;
-const BAR_HEIGHT = 20;
const CHAR_SELECT_WIDTH = 8;
-const UPDATE_INTERVAL = 65;
+const UPDATE_INTERVAL = 60;
+/**
+ * Toggles AO1-style loading using paginated music packets.
+ * (It is unclear why AO2 loading does not work on mobile platforms.)
+ */
let oldLoading = false;
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|BB|PlayBook|IEMobile|Windows Phone|Kindle|Silk|Opera Mini/i.test(navigator.userAgent)) {
oldLoading = true;
}
+let selectedEffect = 0;
+let selectedMenu = 1;
let selectedShout = 0;
+
+const fp = new Fingerprint({
+ canvas: true,
+ ie_activex: true,
+ screen_resolution: true
+});
+
+/** An emulated, semi-unique HDID that is generally safe for HDID bans. */
+const hdid = fp.get();
+console.log(`Your emulated HDID is ${hdid}`);
+
let lastICMessageTime = new Date(0);
-class Client {
+class Client extends EventEmitter {
constructor(address) {
+ super();
this.serv = new WebSocket("ws://" + address);
- this.serv.onopen = (evt) => this.onOpen(evt);
- this.serv.onclose = (evt) => this.onClose(evt);
- this.serv.onmessage = (evt) => this.onMessage(evt);
- this.serv.onerror = (evt) => this.onError(evt);
+ 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"));
+
+ 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));
+
+ this.flip = false;
+ this.presentable = false;
+
+ this.hp = [0, 0];
this.playerID = 1;
this.charID = -1;
+ this.testimonyID = 0;
this.chars = [];
this.emotes = [];
+ this.evidences = [];
+
+ this.resources = {
+ "holdit": {
+ "src": "misc/holdit.gif",
+ "duration": 720
+ },
+ "objection": {
+ "src": "misc/objection.gif",
+ "duration": 720
+ },
+ "takethat": {
+ "src": "misc/takethat.gif",
+ "duration": 840
+ },
+ "witnesstestimony": {
+ "src": "misc/witnesstestimony.gif",
+ "duration": 1560,
+ "sfx": "sounds/general/sfx-testimony.wav"
+ },
+ "crossexamination": {
+ "src": "misc/crossexamination.gif",
+ "duration": 1600,
+ "sfx": "sounds/general/sfx-testimony2.wav"
+ }
+ };
this.selectedEmote = -1;
+ this.selectedEvidence = 0;
this.checkUpdater = null;
// Only used for RMC/`music` packets, not EM/SM/MC packets.
this.musicList = Object();
- this.handlers = {
- "MS": (args) => this.handleMS(args),
- "CT": (args) => this.handleCT(args),
- "MC": (args) => this.handleMC(args),
- "RMC": (args) => this.handleRMC(args),
- "CI": (args) => this.handleCI(args),
- "SC": (args) => this.handleSC(args),
- "EI": (args) => this.handleEI(args),
- "EM": (args) => this.handleEM(args),
- "SM": (args) => this.handleSM(args),
- "BD": (args) => this.handleBD(args),
- "music": (args) => this.handlemusic(args),
- "DONE": (args) => this.handleDONE(args),
- "BN": (args) => this.handleBN(args),
- "NBG": (args) => this.handleNBG(args),
- "HP": (args) => this.handleHP(args),
- "ID": (args) => this.handleID(args),
- "PN": (args) => this.handlePN(args),
- "SI": (args) => this.handleSI(args),
- "CharsCheck": (args) => this.handleCharsCheck(args),
- "PV": (args) => this.handlePV(args),
- "CHECK": (args) => {}
- }
+ 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("LE", this.handleLE.bind(this));
+ this.on("EM", this.handleEM.bind(this));
+ this.on("SM", this.handleSM.bind(this));
+ this.on("BD", this.handleBD.bind(this));
+ this.on("music", this.handlemusic.bind(this));
+ this.on("DONE", this.handleDONE.bind(this));
+ this.on("BN", this.handleBN.bind(this));
+ this.on("NBG", this.handleNBG.bind(this));
+ this.on("HP", this.handleHP.bind(this));
+ this.on("RT", this.handleRT.bind(this));
+ this.on("ZZ", this.handleZZ.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("CharsCheck", this.handleCharsCheck.bind(this));
+ this.on("PV", this.handlePV.bind(this));
+ this.on("CHECK", () => {});
this._lastTimeICReceived = new Date(0);
}
@@ -81,23 +147,30 @@ class Client {
/**
* Gets the current player's character.
*/
- me() {
+ get character() {
return this.chars[this.charID];
}
/**
* Gets the player's currently selected emote.
*/
- myEmote() {
+ get emote() {
return this.emotes[this.selectedEmote];
}
/**
+ * Gets the player's currently selected evidence if presentable.
+ */
+ get evidence() {
+ return this.presentable ? this.selectedEvidence : 0;
+ }
+
+ /**
* Sends an out-of-character chat message.
* @param {string} message the message to send
*/
sendOOC(message) {
- this.serv.send(`CT#web${this.playerID}#${escapeChat(message)}#%`);
+ this.serv.send(`CT#${escapeChat(encodeChat(document.getElementById("OOC_name").value))}#${escapeChat(encodeChat(message))}#%`);
}
/**
@@ -109,18 +182,78 @@ class Client {
* @param {string} side the name of the side in the background
* @param {string} ssfxname the name of the sound effect
* @param {string} zoom whether or not to zoom
- * @param {string} ssfxdelay the delay (in milliseconds) to play the sound effect
+ * @param {number} ssfxdelay the delay (in milliseconds) to play the sound effect
* @param {string} objection the number of the shout to play
+ * @param {string} evidence the filename of evidence to show
+ * @param {number} flip change to 1 to reverse sprite for position changes
+ * @param {string} flash screen flash effect
+ * @param {string} color text color
*/
- sendIC(speaking, name, silent, message, side, ssfxname, zoom, ssfxdelay, objection) {
+ sendIC(speaking, name, silent, message, side, ssfxname, zoom, ssfxdelay, objection, evidence, flip, flash, color) {
this.serv.send(
`MS#chat#${speaking}#${name}#${silent}` +
- `#${escapeChat(message)}#${side}#${ssfxname}#${zoom}` +
- `#${this.charID}#${ssfxdelay}#${selectedShout}#0#0#0#0#%`
+ `#${escapeChat(encodeChat(message))}#${side}#${ssfxname}#${zoom}` +
+ `#${this.charID}#${ssfxdelay}#${selectedShout}#${evidence}#${flip}#${flash}#${color}#%`
);
}
/**
+ * Sends add evidence command.
+ * @param {string} evidence name
+ * @param {string} evidence description
+ * @param {string} evidence image filename
+ */
+ sendPE(name, desc, img) {
+ this.serv.send(`PE#${escapeChat(encodeChat(name))}#${escapeChat(encodeChat(desc))}#${img}#%`);
+ }
+
+ /**
+ * Sends edit evidence command.
+ * @param {number} evidence id
+ * @param {string} evidence name
+ * @param {string} evidence description
+ * @param {string} evidence image filename
+ */
+ sendEE(id, name, desc, img) {
+ this.serv.send(`EE#${id}#${escapeChat(encodeChat(name))}#${escapeChat(encodeChat(desc))}#${img}#%`);
+ }
+
+ /**
+ * Sends delete evidence command.
+ * @param {number} evidence id
+ */
+ sendDE(id) {
+ this.serv.send(`DE#${id}#%`);
+ }
+
+ /**
+ * Sends health point command.
+ * @param {number} side the position
+ * @param {number} hp the health point
+ */
+ sendHP(side, hp) {
+ this.serv.send(`HP#${side}#${hp}#%`);
+ }
+
+ /**
+ * Sends call mod command.
+ * @param {string} message to mod
+ */
+ sendZZ(msg) {
+ this.serv.send(`ZZ#${msg}#%`);
+ }
+
+ /**
+ * Sends testimony command.
+ * @param {string} testimony type
+ */
+ sendRT(testimony) {
+ if (this.chars[this.charID].side === "jud") {
+ this.serv.send(`RT#${testimony}#%`);
+ }
+ }
+
+ /**
* Requests to change the music to the specified track.
* @param {string} track the track ID
*/
@@ -143,12 +276,86 @@ class Client {
* to the server.
*/
joinServer() {
- this.serv.send(`HI#${navigator.userAgent.hashCode()}#%`);
- this.serv.send("ID#webAO#2.4.5#%");
+ this.serv.send(`HI#${hdid}#%`);
+ this.serv.send("ID#webAO#2.3#%");
this.checkUpdater = setInterval(() => this.sendCheck(), 5000);
}
/**
+ * Load game resources.
+ */
+ loadResources() {
+ // Set to playerID to server chat name
+ // TODO: Make a text box for this!
+ document.getElementById("OOC_name").value = "web" + this.playerID;
+
+ // Load evidence array to select
+ const evidence_select = document.getElementById("evi_select");
+ evidence_select.add(new Option("Custom", 0));
+ evidence_arr.forEach(evidence => {
+ evidence_select.add(new Option(evidence));
+ });
+
+ // Load background array to select
+ const background_select = document.getElementById("bg_select");
+ background_select.add(new Option("Custom", 0));
+ background_arr.forEach(background => {
+ background_select.add(new Option(background));
+ });
+
+ this.resources.map(async (resource) => {
+ // Check if image exists and replace `src` with an absolute URL
+ const spriteSrc = `${AO_HOST}themes/default/${resource.src}.gif`;
+ if (await fileExists(spriteSrc)) {
+ Object.assign(resource, {
+ src: spriteSrc,
+ duration: await viewport.getAnimLength(spriteSrc)
+ });
+ }
+
+ // Check if sfx exists and replace `sfx` with an absolute URL
+ if (resource.sfx) {
+ const sfxSrc = AO_HOST + resource.sfx.toLowerCase();
+ if (await fileExists(sfxSrc)) {
+ resource.sfx = sfxSrc;
+ }
+ }
+ });
+ }
+
+ /**
+ * Create observer to detect BBCode elements
+ * then manipulate them.
+ */
+ initialObservBBCode() {
+ const target = document.getElementById("client_inner_chat");
+ const observer = new MutationObserver(mutations => {
+ mutations.forEach(mutation => {
+ const children = mutation.addedNodes;
+ if (children !== null) {
+ children.forEach(function (node) {
+ if (node.tagName === "C") {
+ node.style.color = node.getAttribute("a");
+ } else if (node.tagName === "M") {
+ if (node.hasAttribute("a")) {
+ node.style.backgroundColor = node.getAttribute("a");
+ } else {
+ node.style.backgroundColor = "yellow";
+ node.style.color = "black";
+ }
+ }
+ });
+ }
+ });
+ });
+ const config = {
+ attributes: true,
+ childList: true
+ };
+ observer.observe(target, config);
+ }
+
+ /**
* Requests to play as a specified character.
* @param {number} character the character ID
*/
@@ -174,7 +381,7 @@ class Client {
/**
* Triggered when a connection is established to the server.
*/
- onOpen(e) {
+ onOpen(_e) {
// XXX: Why does watching mean just SITTING there and doing nothing?
if (mode === "watch") {
document.getElementById("client_loading").style.display = "none";
@@ -202,15 +409,14 @@ class Client {
* @param {MessageEvent} e
*/
onMessage(e) {
- let msg = e.data;
+ const msg = e.data;
console.debug(msg);
- let lines = msg.split('%');
- let args = lines[0].split('#');
- let header = args[0];
- let handler = this.handlers[header];
- if (typeof handler !== "undefined") {
- handler(args);
- } else {
+
+ const lines = msg.split("%");
+ const args = lines[0].split("#");
+ const header = args[0];
+
+ if (!this.emit(header, args)) {
console.warn(`Invalid packet header ${header}`);
}
}
@@ -229,10 +435,18 @@ class Client {
cleanup() {
try {
this.serv.close(1001);
- } catch (e) {
- // I don't care if this errors
+ } finally {
+ clearInterval(this.checkUpdater);
}
- clearInterval(this.checkUpdater);
+ }
+
+ /**
+ * XXX: a nasty hack made by gameboyprinter.
+ * @param {string} msg chat message to prepare for display
+ */
+ prepChat(msg){
+ // TODO: make this less awful
+ return decodeBBCode(unescapeChat(decodeChat(msg)));
}
/**
@@ -241,17 +455,17 @@ class Client {
*/
handleMS(args) {
// TODO: this if-statement might be a bug.
- if (args[4] != viewport.chatmsg.content) {
+ if (args[4] !== viewport.chatmsg.content) {
document.getElementById("client_inner_chat").innerHTML = "";
- let chatmsg = {
- pre: escape(args[2]),
+ const chatmsg = {
+ // pre: escape(args[2]),
character: -1, // Will do a linear search
preanim: escape(args[2]), // XXX: why again?
nameplate: args[3], // TODO: parse INI to get this info
name: args[3],
speaking: "(b)" + escape(args[4]),
silent: "(a)" + escape(args[4]),
- content: args[5],
+ content: this.prepChat(args[5]), // Escape HTML tag, Use BBCode Only!
side: args[6],
sound: escape(args[7]),
type: args[8],
@@ -259,7 +473,7 @@ class Client {
snddelay: args[10],
objection: args[11],
evidence: args[12],
- // flip: args[13],
+ flip: args[13],
flash: args[14],
color: args[15],
isnew: true,
@@ -267,17 +481,17 @@ class Client {
// The dreaded linear search...
for (let i = 0; i < this.chars.length; i++) {
- if (this.chars[i].name == args[3]) {
+ if (this.chars[i].name === args[3]) {
chatmsg.character = i;
break;
}
}
- if (chatmsg.character == this.charID) {
+ if (chatmsg.character === this.charID) {
resetICParams();
}
- viewport.say(chatmsg);
+ viewport.say(chatmsg); // no await
}
}
@@ -287,8 +501,8 @@ class Client {
*/
handleCT(args) {
const oocLog = document.getElementById("client_ooclog");
- oocLog.innerHTML += `${args[1]}: ${args[2]}\r\n`;
- if (oocLog.scrollTop > oocLog.scrollHeight - 60) {
+ oocLog.innerHTML += `${decodeChat(unescapeChat(args[1]))}: ${decodeChat(unescapeChat(args[2]))}\r\n`;
+ if (oocLog.scrollTop > oocLog.scrollHeight - 600) {
oocLog.scrollTop = oocLog.scrollHeight;
}
}
@@ -298,15 +512,16 @@ class Client {
* @param {Array} args packet arguments
*/
handleMC(args) {
+ const [ _packet, track, charID ] = args;
const music = viewport.music;
music.pause();
- music.src = MUSIC_HOST + args[1];
+ music.src = MUSIC_HOST + track.toLowerCase();
music.play();
if (args[2] >= 0) {
- let musicname = this.chars[args[2]].name;
- appendICLog(`${musicname} changed music to ${args[1]}`);
+ const musicname = this.chars[charID].name;
+ appendICLog(`${musicname} changed music to ${track}`);
} else {
- appendICLog(`The music was changed to ${args[1]}`);
+ appendICLog(`The music was changed to ${track}`);
}
}
@@ -321,7 +536,7 @@ class Client {
// Music offset + drift from song loading
music.totime = args[1];
music.offset = new Date().getTime() / 1000;
- music.addEventListener('loadedmetadata', function() {
+ music.addEventListener("loadedmetadata", function () {
music.currentTime += parseFloat(music.totime + (new Date().getTime() / 1000 - music.offset)).toFixed(3);
music.play();
}, false);
@@ -336,13 +551,13 @@ class Client {
document.getElementById("client_loadingtext").innerHTML = "Loading Character " + args[1];
this.serv.send("AN#" + ((args[1] / 10) + 1) + "#%");
for (let i = 2; i < args.length - 1; i++) {
- if (i % 2 == 0) {
- let chargs = args[i].split("&");
+ if (i % 2 === 0) {
+ const chargs = args[i].split("&");
this.chars[args[i - 1]] = {
- "name": chargs[0],
- "desc": chargs[1],
- "evidence": chargs[3],
- "icon": AO_HOST + "characters/" + escape(chargs[0]) + "/char_icon.png"
+ name: chargs[0],
+ desc: chargs[1],
+ evidence: chargs[3],
+ icon: AO_HOST + "characters/" + escape(chargs[0].toLowerCase()) + "/char_icon.png"
};
}
}
@@ -356,13 +571,13 @@ class Client {
handleSC(args) {
document.getElementById("client_loadingtext").innerHTML = "Loading Characters";
for (let i = 1; i < args.length - 1; i++) {
- let chargs = args[i].split("&");
+ const chargs = args[i].split("&");
this.chars[i - 1] = {
- "name": chargs[0],
- "desc": chargs[1],
- "evidence": chargs[3],
- "icon": AO_HOST + "characters/" + escape(chargs[0]) + "/char_icon.png"
- }
+ name: chargs[0],
+ desc: chargs[1],
+ evidence: chargs[3],
+ icon: AO_HOST + "characters/" + escape(chargs[0].toLowerCase()) + "/char_icon.png"
+ };
}
this.serv.send("RM#%");
}
@@ -381,6 +596,35 @@ class Client {
}
/**
+ * Handles incoming evidence list, all evidences at once
+ * item per packet.
+ *
+ * @param {Array} args packet arguments
+ */
+ handleLE(args) {
+ this.evidences = [];
+ for (let i = 1; i < args.length - 1; i++) {
+ const arg = args[i].split("&");
+ this.evidences[i - 1] = {
+ name: decodeChat(unescapeChat(arg[0])),
+ desc: decodeChat(unescapeChat(arg[1])),
+ filename: escape(arg[2]),
+ icon: AO_HOST + "evidence/" + escape(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="client_button"
+ onclick="pickEvidence(${i})">`;
+ }
+ }
+
+ /**
* Handles incoming music information, containing multiple entries
* per packet.
* @param {Array} args packet arguments
@@ -388,10 +632,10 @@ class Client {
handleEM(args) {
document.getElementById("client_loadingtext").innerHTML = "Loading Music " + args[1];
this.serv.send("AM#" + ((args[1] / 10) + 1) + "#%");
- let hmusiclist = document.getElementById("client_musiclist");
+ const hmusiclist = document.getElementById("client_musiclist");
for (let i = 2; i < args.length - 1; i++) {
- if (i % 2 == 0) {
- let newentry = document.createElement("OPTION");
+ if (i % 2 === 0) {
+ const newentry = document.createElement("OPTION");
newentry.text = args[i];
hmusiclist.options.add(newentry);
}
@@ -405,12 +649,42 @@ class Client {
*/
handleSM(args) {
document.getElementById("client_loadingtext").innerHTML = "Loading Music ";
- let hmusiclist = document.getElementById("client_musiclist");
+ const hmusiclist = document.getElementById("client_musiclist");
+ let flagAudio = false;
+
for (let i = 1; i < args.length - 1; i++) {
- let newentry = document.createElement("OPTION");
- newentry.text = args[i];
- hmusiclist.options.add(newentry);
+ // Check when found the song for the first time
+ if (/\.(?:wav|mp3|mp4|ogg|opus)$/i.test(args[i]) && !flagAudio) {
+ flagAudio = true;
+ }
+
+ if (flagAudio) {
+ // After reached the audio put everything in the music list
+ const newentry = document.createElement("OPTION");
+ newentry.text = args[i];
+ hmusiclist.options.add(newentry);
+ } else {
+ // Create area button
+ const newarea = document.createElement("SPAN");
+ newarea.className = "location-box";
+ newarea.textContent = args[i];
+ newarea.onclick = function () {
+ area_click(this);
+ };
+ document.getElementById("areas").appendChild(newarea);
+ }
}
+
+ // We need to check if the last area that we got was actually a category
+ // header for music. If it was, then move it over to the music list.
+ const area_box = document.getElementById("areas");
+ if (area_box.lastChild.textContent.startsWith("=")) {
+ const audio_title = document.createElement("OPTION");
+ audio_title.text = area_box.lastChild.textContent;
+ hmusiclist.insertBefore(audio_title, hmusiclist.firstChild);
+ area_box.removeChild(area_box.lastChild);
+ }
+
this.serv.send("RD#%");
}
@@ -423,6 +697,14 @@ class Client {
}
/**
+ * Handles the banned packet
+ * @param {Array} args packet arguments
+ */
+ handleBD(args) {
+ document.getElementById("client_loadingtext").innerHTML = "Banned: " + args[1];
+ }
+
+ /**
* Handles incoming music information, containing all entries
* in the same packet.
* @param {Array} args packet arguments
@@ -439,7 +721,7 @@ class Client {
*
* @param {Array} args packet arguments
*/
- handleDONE(args) {
+ handleDONE(_args) {
document.getElementById("client_loading").style.display = "none";
document.getElementById("client_charselect").style.display = "block";
}
@@ -450,9 +732,22 @@ class Client {
*/
handleBN(args) {
viewport.bgname = escape(args[1]);
+ const bg_index = getIndexFromSelect("bg_select", escape(args[1]));
+ document.getElementById("bg_select").selectedIndex = bg_index;
+ updateBackgroundPreview();
+ if (bg_index === 0) {
+ document.getElementById("bg_filename").value = args[1];
+ }
+ document.getElementById("bg_preview").src = AO_HOST + "background/" + escape(args[1].toLowerCase()) + "/defenseempty.png";
+ if (this.charID === -1) {
+ changeBackground("jud");
+ } else {
+ changeBackground(this.chars[this.charID].side);
+ }
+
}
- handleNBG(args) {
+ handleNBG(_args) {
// TODO (set by sD)
}
@@ -461,15 +756,49 @@ class Client {
* @param {Array} args packet arguments
*/
handleHP(args) {
- // TODO (set by sD)
- // Also, this is broken.
- if (args[1] == 1) {
- document.getElementById("client_defense_hp").style.clip = "rect(0px," + BAR_WIDTH * args[2] / 10 + "px," + BAR_HEIGHT + "px,0px)";
+ const percent_hp = args[2] * 10;
+ if (args[1] === 1) {
+ // Def hp
+ this.hp[0] = args[2];
+ $("#client_defense_hp > .health-bar").animate({
+ "width": percent_hp + "%"
+ }, 500);
} else {
- document.getElementById("client_prosecutor_hp").style.clip = "rect(0px," + BAR_WIDTH * args[2] / 10 + "px," + BAR_HEIGHT + "px,0px)";
+ // Pro hp
+ this.hp[1] = args[2];
+ $("#client_prosecutor_hp > .health-bar").animate({
+ "width": percent_hp + "%"
+ }, 500);
}
}
-
+
+ /**
+ * Handles a testimony states.
+ * @param {Array} args packet arguments
+ */
+ handleRT(args) {
+ if (args[1] === "testimony1") {
+ //Witness Testimony
+ this.testimonyID = 1;
+ } else {
+ //Cross Examination
+ this.testimonyID = 2;
+ }
+ viewport.initTestimonyUpdater();
+ }
+
+ /**
+ * Handles a call mod message.
+ * @param {Array} args packet arguments
+ */
+ handleZZ(args) {
+ const oocLog = document.getElementById("client_ooclog");
+ oocLog.innerHTML += `$Alert: ${decodeChat(unescapeChat(args[1]))}\r\n`;
+ if (oocLog.scrollTop > oocLog.scrollHeight - 60) {
+ oocLog.scrollTop = oocLog.scrollHeight;
+ }
+ }
+
/**
* Handles the issuance of a player ID by the server.
* @param {Array} args packet arguments
@@ -478,7 +807,7 @@ class Client {
this.playerID = args[1];
}
- handlePN(args) {
+ handlePN(_args) {
this.serv.send("askchaa#%");
}
@@ -487,7 +816,7 @@ class Client {
* but we use it as a cue to begin retrieving characters.
* @param {Array} args packet arguments
*/
- handleSI(args) {
+ handleSI(_args) {
if (oldLoading) {
this.serv.send("askchar2#%");
} else {
@@ -501,72 +830,71 @@ class Client {
*/
handleCharsCheck(args) {
document.getElementById("client_chartable").innerHTML = "";
+ let tr;
for (let i = 0; i < this.chars.length; i++) {
- if (i % CHAR_SELECT_WIDTH == 0) {
- var tr = document.createElement('TR');
+ if (i % CHAR_SELECT_WIDTH === 0) {
+ tr = document.createElement("TR");
}
- let td = document.createElement('TD');
- let icon_chosen;
- let thispick = this.chars[i].icon;
- if (args[i + 1] == "-1") {
+ const td = document.createElement("TD");
+ let icon_chosen = "";
+ const thispick = this.chars[i].icon;
+ if (args[i + 1] === "-1") {
icon_chosen = " dark";
- } else {
- icon_chosen = "";
}
td.innerHTML = `<img class='demothing${icon_chosen}' id='demo_${i}' ` +
- `src='${thispick}' alt='${this.chars[i].name}' onclick='pickchar(${i})' ` +
- `onerror='demoError(this);'>`;
+ `src='${thispick}' alt='${this.chars[i].name}' onclick='pickChar(${i})' ` +
+ "onerror='demoError(this);'>";
tr.appendChild(td);
- if (i % CHAR_SELECT_WIDTH == 0) {
+ if (i % CHAR_SELECT_WIDTH === 0) {
document.getElementById("client_chartable").appendChild(tr);
}
}
- changeBackground("def");
+ //changeBackground("def");
}
/**
* Handles the server's assignment of a character for the player to use.
* @param {Array} args packet arguments
*/
- handlePV(args) {
+ async handlePV(args) {
this.charID = args[3];
+
document.getElementById("client_charselect").style.display = "none";
- let me = this.me();
- let emotes = this.emotes;
- let xhr = new XMLHttpRequest();
- xhr.open('GET', AO_HOST + 'characters/' + escape(this.me().name) + '/char.ini', true);
- xhr.responseType = 'text';
- xhr.onload = function (e) {
- if (this.status == 200) {
- let linifile = this.responseText;
- let pinifile = INI.parse(linifile);
- me.side = pinifile.Options.side;
- for (let i = 1; i < pinifile.Emotions.number; i++) {
- let emoteinfo = pinifile.Emotions[i].split('#');
- let esfx = "0";
- let esfxd = "0";
- if (typeof pinifile.SoundN !== 'undefined') {
- esfx = pinifile.SoundN[i];
- }
- if (typeof pinifile.SoundT !== 'undefined') {
- esfxd = pinifile.SoundT[i];
- }
- emotes[i] = {
- desc: emoteinfo[0],
- speaking: emoteinfo[1],
- silent: emoteinfo[2],
- zoom: emoteinfo[3],
- sfx: esfx,
- sfxdelay: esfxd,
- button_off: AO_HOST + 'characters/' + escape(me.name) + '/emotions/button' + i + '_off.png',
- button_on: AO_HOST + 'characters/' + escape(me.name) + '/emotions/button' + i + '_on.png'
- };
- document.getElementById("client_emo").innerHTML += "<img src='" + emotes[i].button_off + "' id='emo_" + i + "' alt='" + emotes[i].desc + "' class='client_button' onclick='pickemotion(" + i + ")'>";
- }
- pickemotion(1);
- }
- };
- xhr.send();
+ document.getElementById("client_inputbox").style.display = "";
+
+ const me = this.character;
+ const emotes = this.emotes;
+ const emotesList = document.getElementById("client_emo");
+ emotesList.innerHTML = ""; // Clear emote box
+ emotesList.style.display = "";
+
+ const data = await request(AO_HOST + "characters/" + escape(this.character.name.toLowerCase()) + "/char.ini");
+ const ini = INI.parse(data);
+ me.side = ini.Options.side;
+ updateActionCommands(me.side);
+ for (let i = 1; i <= ini.Emotions.number; i++) {
+ const emoteinfo = ini.Emotions[i].split("#");
+ const esfx = ini.SoundN ? ini.SoundN[i] : "0";
+ const esfxd = ini.SoundT ? ini.SoundT[i] : "0";
+ // Make sure the asset server is case insensitive, or that everything on it is lowercase
+ emotes[i] = {
+ desc: emoteinfo[0].toLowerCase(),
+ speaking: emoteinfo[1].toLowerCase(),
+ silent: emoteinfo[2].toLowerCase(),
+ zoom: emoteinfo[3],
+ sfx: esfx.toLowerCase(),
+ sfxdelay: esfxd,
+ button_off: AO_HOST + `characters/${escape(me.name).toLowerCase()}/emotions/button${i}_off.png`,
+ button_on: AO_HOST + `characters/${escape(me.name).toLowerCase()}/emotions/button${i}_on.png`
+ };
+ emotesList.innerHTML +=
+ `<img src=${emotes[i].button_off}
+ id="emo_${i}"
+ alt="${emotes[i].desc}"
+ class="client_button"
+ onclick="pickEmotion(${i})">`;
+ }
+ pickEmotion(1);
}
}
@@ -578,34 +906,36 @@ class Viewport {
"content": "",
"objection": "0",
"sound": "",
+ "startpreanim": false,
"startspeaking": false,
"side": null,
"color": "0",
- "snddelay": 0
+ "snddelay": 0,
+ "preanimdelay": 0
};
- this.blip = new Audio(AO_HOST + 'sounds/general/sfx-blipmale.wav');
+ this.blip = new Audio(AO_HOST + "sounds/general/sfx-blipmale.wav");
this.blip.volume = 0.5;
// Allocate multiple blip audio channels to make blips less jittery
// TODO: read blip type ("gender") from ini
this.blipChannels = new Array(6);
- for (let i = 0; i < this.blipChannels.length; i++) {
- this.blipChannels[i] = new Audio(AO_HOST + 'sounds/general/sfx-blipmale.wav');
- this.blipChannels[i].volume = 0.5;
- }
+ this.blipChannels.fill(new Audio(AO_HOST + "sounds/general/sfx-blipmale.wav"))
+ .forEach(channel => channel.volume = 0.5);
this.currentBlipChannel = 0;
- this.sfxaudio = new Audio(AO_HOST + 'sounds/general/sfx-blipmale.wav');
+ this.sfxaudio = new Audio(AO_HOST + "sounds/general/sfx-blipmale.wav");
this.sfxplayed = 0;
this.music = new Audio();
this.music.play();
this.updater = null;
+ this.testimonyUpdater = null;
this.bgname = "gs4";
+ this.testimonyTimer = 0;
this.shoutTimer = 0;
this.textTimer = 0;
@@ -616,7 +946,7 @@ class Viewport {
* Returns whether or not the viewport is busy
* performing a task (animating).
*/
- isAnimating() {
+ get isAnimating() {
return this._animating;
}
@@ -624,33 +954,122 @@ class Viewport {
* Sets the volume of the blip sound.
* @param {number} volume
*/
- setBlipVolume(volume) {
- for (let i = 0; i < this.blipChannels.length; i++) {
- this.blipChannels[i].volume = volume;
- }
+ set blipVolume(volume) {
+ this.blipChannels.forEach(channel => channel.volume = volume);
}
/**
* Returns the path which the background is located in.
*/
- bgFolder() {
- return `${AO_HOST}background/${this.bgname}/`;
+ get bgFolder() {
+ return `${AO_HOST}background/${this.bgname.toLowerCase()}/`;
}
/**
* Sets a new emote.
* @param {object} chatmsg the new chat message
*/
- say(chatmsg) {
+ async say(chatmsg) {
this.chatmsg = chatmsg;
appendICLog(chatmsg.content, chatmsg.nameplate);
changeBackground(chatmsg.side);
- this.textnow = '';
+ this.textnow = "";
this.sfxplayed = 0;
this.textTimer = 0;
this._animating = true;
clearTimeout(this.updater);
- this.updater = setTimeout(() => this.updateText(), UPDATE_INTERVAL);
+ // If preanim existed then determine the length
+ if (chatmsg.preanim !== "-") {
+ const delay = await this.getAnimLength(`${AO_HOST}characters/${escape(chatmsg.name.toLowerCase())}/${chatmsg.preanim.toLowerCase()}.gif`);
+ chatmsg.preanimdelay = delay;
+ this.initUpdater(delay);
+ } else {
+ this.initUpdater(0);
+ }
+ }
+
+ /**
+ * Intialize updater
+ * @param {number} animdelay the length of pre-animation
+ */
+ initUpdater(animdelay) {
+ viewport.chatmsg.preanimdelay = parseInt(animdelay);
+ viewport.updater = setTimeout(() => viewport.tick(), UPDATE_INTERVAL);
+ }
+
+ /**
+ * Intialize testimony updater
+ */
+ initTestimonyUpdater() {
+ const testimonyFilenames = {
+ 1: "witnesstestimony",
+ 2: "crossexamination"
+ };
+
+ const testimony = testimonyFilenames[client.testimonyID];
+ if (!testimony) {
+ console.warn(`Invalid testimony ID ${client.testimonyID}`);
+ return;
+ }
+
+ (new Audio(client.resources[testimony].sfx)).play();
+
+ const testimonyOverlay = document.getElementById("client_testimony");
+ testimonyOverlay.src = client.resources[testimony].src;
+ testimonyOverlay.style.display = "";
+
+ this.testimonyTimer = 0;
+ this.testimonyUpdater = setTimeout(() => this.updateTestimony(), UPDATE_INTERVAL);
+ }
+
+ /**
+ * Gets animation length. If the animation cannot be found, it will
+ * silently fail and return 0 instead.
+ * @param {string} filename the animation file name
+ */
+ async getAnimLength(filename) {
+ try {
+ const file = await request(filename);
+ return gify.getInfo(file).duration;
+ } catch (err) {
+ return 0;
+ }
+ }
+
+ /**
+ * Updates the testimony overaly
+ */
+ updateTestimony() {
+ const testimonyFilenames = {
+ 1: "witnesstestimony",
+ 2: "crossexamination"
+ };
+
+ // Update timer
+ this.testimonyTimer = 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.display = "none";
+ clearTimeout(this.testimonyUpdater);
}
/**
@@ -658,46 +1077,119 @@ class Viewport {
*
* XXX: This relies on a global variable `this.chatmsg`!
*/
- updateText() {
- if (this.chatmsg.content.trim() == "") {
- document.getElementById("client_name").style.display = "none";
- document.getElementById("client_chat").style.display = "none";
+ tick() {
+ const nameBox = document.getElementById("client_name");
+ const chatBox = document.getElementById("client_chat");
+ const charSprite = document.getElementById("client_char");
+ const eviBox = document.getElementById("client_evi");
+ const background = document.getElementById("client_background");
+ const shoutSprite = document.getElementById("client_shout");
+ const chatBoxInner = document.getElementById("client_inner_chat");
+
+ // Flip the character
+ if (this.chatmsg.flip === 1) {
+ charSprite.style.transform = "scaleX(-1)";
} else {
- document.getElementById("client_name").style.display = "block";
- document.getElementById("client_chat").style.display = "block";
+ charSprite.style.transform = "scaleX(1)";
}
if (this._animating) {
- this.updater = setTimeout(() => this.updateText(), UPDATE_INTERVAL);
+ this.updater = setTimeout(() => this.tick(), UPDATE_INTERVAL);
}
if (this.chatmsg.isnew) {
+ // Reset screen background
+ background.style.backgroundColor = "transparent";
+ // Hide message and evidence window
+ nameBox.style.display = "none";
+ chatBox.style.display = "none";
+ eviBox.style.opacity = "0";
+ eviBox.style.height = "0%";
const shouts = {
"1": "holdit",
- "2": "takethat",
- "3": "objection"
+ "2": "objection",
+ "3": "takethat"
};
- let shout = shouts[this.chatmsg.objection];
- if (typeof shout !== "undefined") {
- document.getElementById("client_char").src = AO_HOST + "misc/" + shout + ".gif";
- (new Audio(`${AO_HOST}/characters/${this.chatmsg.name}/${shout}.wav`)).play();
- this.shoutTimer = 800;
+ const shout = shouts[this.chatmsg.objection];
+ if (shout) {
+ shoutSprite.src = client.resources[shout]["src"];
+ (new Audio(`${AO_HOST}characters/${this.chatmsg.name.toLowerCase()}/${shout}.wav`)).play();
+ this.shoutTimer = 850;
} else {
this.shoutTimer = 0;
}
this.chatmsg.isnew = false;
- this.chatmsg.startspeaking = true;
+ this.chatmsg.startpreanim = true;
}
- if (this.textTimer >= this.shoutTimer) {
- if (this.chatmsg.startspeaking) {
+ if (this.textTimer >= this.shoutTimer && this.chatmsg.startpreanim) {
+ // Effect stuff
+ if (this.chatmsg.flash === 2) {
+ // Shake screen
+ this.sfxaudio.pause();
+ this.sfxplayed = 1;
+ this.sfxaudio.src = AO_HOST + "sounds/general/sfx-stab.wav";
+ this.sfxaudio.play();
+ $("#client_gamewindow").effect("shake", {
+ "direction": "up"
+ });
+ } else if (this.chatmsg.flash === 1) {
+ // Flash screen
+ background.style.backgroundColor = "white";
+ this.sfxaudio.pause();
+ this.sfxplayed = 1;
+ this.sfxaudio.src = AO_HOST + "sounds/general/sfx-realization.wav";
+ this.sfxaudio.play();
+ $("#client_gamewindow").effect("pulsate");
+ }
+
+ // Pre-animation stuff
+ if (this.chatmsg.preanimdelay > 0) {
+ shoutSprite.src = "misc/placeholder.gif";
changeBackground(this.chatmsg.side);
- document.getElementById("client_char").src = AO_HOST + "characters/" + escape(this.chatmsg.name) + "/" + this.chatmsg.speaking + ".gif";
- document.getElementById("client_name").style.fontSize = (document.getElementById("client_name").offsetHeight * 0.7) + "px";
- document.getElementById("client_chat").style.fontSize = (document.getElementById("client_chat").offsetHeight * 0.25) + "px";
- document.getElementById("client_name").innerHTML = "<p>" + escapeHtml(this.chatmsg.nameplate) + "</p>";
+ const charName = escape(this.chatmsg.name.toLowerCase());
+ const preanim = this.chatmsg.preanim.toLowerCase();
+ charSprite.src = `${AO_HOST}characters/${charName}/${preanim}.gif`;
+ }
+
+ this.chatmsg.startpreanim = false;
+ this.chatmsg.startspeaking = true;
+ } else if (this.textTimer >= this.shoutTimer + this.chatmsg.preanimdelay && !this.chatmsg.startpreanim) {
+ if (this.chatmsg.startspeaking) {
+ if (this.chatmsg.evidence > 0) {
+ // Prepare evidence
+ eviBox.style.backgroundImage = "url('" + client.evidences[this.chatmsg.evidence - 1].icon + "')";
+
+ if (this.chatmsg.side === "def") {
+ // Only def show evidence on right
+ eviBox.style.right = "1.5em";
+ eviBox.style.left = "initial";
+ $("#client_evi").animate({
+ height: "30%",
+ opacity: 1
+ }, 250);
+ } else {
+ eviBox.style.right = "initial";
+ eviBox.style.left = "1.5em";
+ $("#client_evi").animate({
+ height: "30%",
+ opacity: 1
+ }, 250);
+ }
+ }
+
+ nameBox.style.display = "block";
+ nameBox.style.fontSize = (nameBox.offsetHeight * 0.7) + "px";
+
+ while (nameBox.hasChildNodes()) {
+ nameBox.removeChild(nameBox.firstChild);
+ }
+ nameBox.appendChild(document.createTextNode(this.chatmsg.nameplate));
+
+ chatBox.style.display = "block";
+ chatBox.style.fontSize = (chatBox.offsetHeight * 0.25) + "px";
const colors = {
"0": "#ffffff",
@@ -707,39 +1199,51 @@ class Viewport {
"4": "#0000ff",
"5": "#ffff00",
"6": "#aa00aa"
- }
- let stylecolor = "color: " + (colors[this.chatmsg.color] || "#ffffff");
- document.getElementById("client_inner_chat").style = stylecolor;
+ };
+ chatBoxInner.style.color = colors[this.chatmsg.color] || "#ffffff";
this.chatmsg.startspeaking = false;
- if (this.textnow == this.chatmsg.content) {
- document.getElementById("client_char").src = AO_HOST + "characters/" + escape(this.chatmsg.name) + "/" + this.chatmsg.silent + ".gif";
+ if (this.chatmsg.preanimdelay === 0) {
+ shoutSprite.src = "misc/placeholder.gif";
+ changeBackground(this.chatmsg.side);
+ }
+
+ charSprite.src = AO_HOST + "characters/" + escape(this.chatmsg.name.toLowerCase()) + "/" + this.chatmsg.speaking.toLowerCase() + ".gif";
+
+ if (this.textnow === this.chatmsg.content) {
+ charSprite.src = AO_HOST + "characters/" + escape(this.chatmsg.name.toLowerCase()) + "/" + this.chatmsg.silent.toLowerCase() + ".gif";
this._animating = false;
clearTimeout(this.updater);
}
} else {
- if (this.textnow != this.chatmsg.content) {
- if (this.chatmsg.content.charAt(this.textnow.length) != " ") {
+ if (this.textnow !== this.chatmsg.content) {
+ if (this.chatmsg.content.charAt(this.textnow.length) !== " ") {
this.blipChannels[this.currentBlipChannel].play();
this.currentBlipChannel++;
this.currentBlipChannel %= this.blipChannels.length;
}
this.textnow = this.chatmsg.content.substring(0, this.textnow.length + 1);
- document.getElementById("client_inner_chat").innerHTML = this.textnow;
- if (this.textnow == this.chatmsg.content) {
+
+ while (chatBoxInner.hasChildNodes()) {
+ chatBoxInner.removeChild(chatBoxInner.firstChild);
+ }
+ chatBoxInner.appendChild(document.createTextNode(this.textnow));
+
+ if (this.textnow === this.chatmsg.content) {
this.textTimer = 0;
this._animating = false;
- document.getElementById("client_char").src = AO_HOST + "characters/" + escape(this.chatmsg.name) + "/" + this.chatmsg.silent + ".gif";
+ charSprite.src = AO_HOST + "characters/" + escape(this.chatmsg.name.toLowerCase()) + "/" + this.chatmsg.silent.toLowerCase() + ".gif";
clearTimeout(this.updater);
}
}
}
}
+
if (!this.sfxplayed && this.chatmsg.snddelay + this.shoutTimer >= this.textTimer) {
this.sfxaudio.pause();
this.sfxplayed = 1;
- if (this.chatmsg.sound != "0" && this.chatmsg.sound != "1") {
- this.sfxaudio.src = AO_HOST + "sounds/general/" + escape(this.chatmsg.sound) + ".wav";
+ if (this.chatmsg.sound !== "0" && this.chatmsg.sound !== "1") {
+ this.sfxaudio.src = AO_HOST + "sounds/general/" + escape(this.chatmsg.sound.toLowerCase()) + ".wav";
this.sfxaudio.play();
}
}
@@ -749,31 +1253,31 @@ class Viewport {
class INI {
static parse(data) {
- let regex = {
+ const regex = {
section: /^\s*\[\s*([^\]]*)\s*\]\s*$/,
- param: /^\s*([\w\.\-\_]+)\s*=\s*(.*?)\s*$/,
+ param: /^\s*([\w.\-_]+)\s*=\s*(.*?)\s*$/,
comment: /^\s*;.*$/
};
- let value = {};
- let lines = data.split(/\r\n|\r|\n/);
- let section = null;
- lines.forEach(function(line) {
+ const value = {};
+ const lines = data.split(/\r\n|\r|\n/);
+ let section;
+ lines.forEach(function (line) {
if (regex.comment.test(line)) {
return;
- } else if (line.length == 0) {
+ } else if (line.length === 0) {
return;
} else if (regex.param.test(line)) {
- let match = line.match(regex.param);
+ const match = line.match(regex.param);
if (section) {
value[section][match[1]] = match[2];
} else {
value[match[1]] = match[2];
}
} else if (regex.section.test(line)) {
- let match = line.match(regex.section);
+ const match = line.match(regex.section);
value[match[1]] = {};
section = match[1];
- };
+ }
});
return value;
}
@@ -784,7 +1288,7 @@ class INI {
* @param {KeyboardEvent} event
*/
export function onOOCEnter(event) {
- if (event.keyCode == 13) {
+ if (event.keyCode === 13) {
client.sendOOC(document.getElementById("client_oocinputbox").value);
document.getElementById("client_oocinputbox").value = "";
}
@@ -796,16 +1300,23 @@ window.onOOCEnter = onOOCEnter;
* @param {KeyboardEvent} event
*/
export function onEnter(event) {
- if (event.keyCode == 13) {
- let mychar = client.me();
- let myemo = client.myEmote();
+ if (event.keyCode === 13) {
+ const mychar = client.character;
+ const myemo = client.emote;
+ const myevi = client.evidence;
+ const myflip = ((client.flip) ? 1 : 0);
+ const mycolor = document.getElementById("textcolor").value;
let ssfxname = "0";
let ssfxdelay = "0";
if (document.getElementById("sendsfx").checked) {
ssfxname = myemo.sfx;
ssfxdelay = myemo.sfxdelay;
}
- client.sendIC(myemo.speaking, mychar.name, myemo.silent, document.getElementById("client_inputbox").value, mychar.side, ssfxname, myemo.zoom, ssfxdelay, selectedShout);
+
+ client.sendIC(myemo.speaking, mychar.name, myemo.silent,
+ document.getElementById("client_inputbox").value, mychar.side,
+ ssfxname, myemo.zoom, ssfxdelay, selectedShout, myevi, myflip,
+ selectedEffect, mycolor);
}
}
window.onEnter = onEnter;
@@ -817,6 +1328,10 @@ window.onEnter = onEnter;
*/
function resetICParams() {
document.getElementById("client_inputbox").value = "";
+ if (selectedEffect) {
+ document.getElementById("button_effect_" + selectedEffect).className = "client_button";
+ selectedEffect = 0;
+ }
if (selectedShout) {
document.getElementById("button_" + selectedShout).className = "client_button";
selectedShout = 0;
@@ -827,13 +1342,28 @@ function resetICParams() {
* Triggered when an item on the music list is clicked.
* @param {MouseEvent} event
*/
-export function musiclist_click(event) {
- let playtrack = document.getElementById("client_musiclist").value;
+export function musiclist_click(_event) {
+ const playtrack = document.getElementById("client_musiclist").value;
client.sendMusicChange(playtrack);
}
window.musiclist_click = musiclist_click;
/**
+ * Triggered when an item on the area list is clicked.
+ * @param {MouseEvent} event
+ */
+export function area_click(el) {
+ const area = el.textContent;
+ 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() {
@@ -853,7 +1383,7 @@ window.changeSFXVolume = changeSFXVolume;
* Triggered by the blip volume slider.
*/
export function changeBlipVolume() {
- viewport.setBlipVolume(document.getElementById("client_bvolume").value / 100);
+ viewport.blipVolume = document.getElementById("client_bvolume").value / 100;
}
window.changeBlipVolume = changeBlipVolume;
@@ -861,7 +1391,7 @@ window.changeBlipVolume = changeBlipVolume;
* Triggered when a character icon is clicked in the character selection menu.
* @param {MouseEvent} event
*/
-export function changeCharacter(event) {
+export function changeCharacter(_event) {
client.sendLeaveRoom();
document.getElementById("client_charselect").style.display = "block";
document.getElementById("client_emo").innerHTML = "";
@@ -874,7 +1404,7 @@ window.changeCharacter = changeCharacter;
*/
export function imgError(image) {
image.onerror = "";
- image.src = "/misc/placeholder.gif";
+ image.src = "misc/placeholder.gif";
return true;
}
window.imgError = imgError;
@@ -891,13 +1421,50 @@ export function demoError(image) {
window.demoError = demoError;
/**
- * Checks if an image exists at the specified URI.
+ * Make a GET request for a specific URI.
+ * @param {string} url the URI to be requested
+ * @returns response data
+ * @throws {Error} if status code is not 2xx, or a network error occurs
+ */
+async function request(url) {
+ return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.addEventListener("error", () => {
+ const err = new Error(`Request for ${url} failed: ${xhr.statusText}`);
+ err.code = xhr.status;
+ reject(err);
+ });
+ xhr.addEventListener("abort", () => {
+ const err = new Error(`Request for ${url} was aborted!`);
+ err.code = xhr.status;
+ reject(err);
+ });
+ xhr.addEventListener("load", () => {
+ if (xhr.status < 200 || xhr.status >= 300) {
+ const err = new Error(`Request for ${url} failed with status code ${xhr.status}`);
+ err.code = xhr.status;
+ reject(err);
+ } else {
+ resolve(xhr.response);
+ }
+ });
+ xhr.open("GET", url, true);
+ xhr.send();
+ });
+}
+
+/**
+ * Checks if a file exists at the specified URI.
* @param {string} url the URI to be checked
*/
-function ImageExist(url) {
- var img = new Image();
- img.src = url;
- return img.height != 0;
+async function fileExists(url) {
+ try {
+ await request(url);
+ return true;
+ } catch (err) {
+ if (err.code >= 400) return false;
+ else throw err;
+ }
}
/**
@@ -906,46 +1473,56 @@ function ImageExist(url) {
* Valid positions: `def, pro, hld, hlp, wit, jud`
* @param {string} position the position to change into
*/
-function changeBackground(position) {
- var standname;
- let bgfolder = viewport.bgFolder();
+async function changeBackground(position) {
+ const bgfolder = viewport.bgFolder;
+
+ const positions = {
+ def: {
+ bg: "defenseempty.png",
+ desk: { ao2: "defensedesk.png", ao1: "bancodefensa.png" },
+ speedLines: "defense_speedlines.gif"
+ },
+ pro: {
+ bg: "prosecutorempty.png",
+ desk: { ao2: "prosecutiondesk.png", ao1: "bancoacusacion.png" },
+ speedLines: "prosecution_speedlines.gif"
+ },
+ hld: {
+ bg: "helperstand.png",
+ desk: null,
+ speedLines: "defense_speedlines.gif"
+ },
+ hlp: {
+ bg: "prohelperstand.png",
+ desk: null,
+ speedLines: "prosecution_speedlines.gif"
+ },
+ wit: {
+ bg: "witnessempty.png",
+ desk: { ao2: "stand.png", ao1: "estrado.png" },
+ speedLines: "prosecution_speedlines.gif"
+ },
+ jud: {
+ bg: "judgestand.png",
+ desk: null,
+ speedLines: "prosecution_speedlines.gif"
+ }
+ };
+
+ const { bg, desk, speedLines } = positions[position];
document.getElementById("client_fg").style.display = "none";
- document.getElementById("client_bench").style.display = "none";
- switch (position) {
- case "def":
- document.getElementById("client_court").src = bgfolder + "defenseempty.png"
+
+ if (viewport.chatmsg.type === 5) {
+ document.getElementById("client_court").src = `${AO_HOST}themes/default/${speedLines}`;
+ } else {
+ document.getElementById("client_court").src = bgfolder + bg;
+ if (desk) {
+ const deskFilename = await fileExists(bgfolder + desk.ao2) ? desk.ao2 : desk.ao1;
+ document.getElementById("client_bench").src = bgfolder + deskFilename;
document.getElementById("client_bench").style.display = "block";
- document.getElementById("client_bench").src = bgfolder + "defensedesk.png"
- standname = "defense";
- break;
- case "pro":
- document.getElementById("client_court").src = bgfolder + "prosecutorempty.png"
- document.getElementById("client_bench").style.display = "block"
- document.getElementById("client_bench").src = bgfolder + "prosecutiondesk.png"
- standname = "prosecution";
- break;
- case "hld":
- document.getElementById("client_court").src = bgfolder + "helperstand.png"
- standname = "defense";
- break;
- case "hlp":
- document.getElementById("client_court").src = bgfolder + "prohelperstand.png"
- standname = "prosecution";
- break;
- case "wit":
- document.getElementById("client_court").src = bgfolder + "witnessempty.png"
- document.getElementById("client_bench").style.display = "block"
- document.getElementById("client_bench").src = bgfolder + "estrado.png"
- standname = "prosecution";
- break;
- case "jud":
- document.getElementById("client_court").src = bgfolder + "judgestand.png"
- standname = "prosecution";
- break;
- }
- if (viewport.chatmsg.type == 5) {
- document.getElementById("client_bench").style.display = "none";
- document.getElementById("client_court").src = AO_HOST + "themes/default/" + standname + "_speedlines.gif";
+ } else {
+ document.getElementById("client_bench").style.display = "none";
+ }
}
}
@@ -972,16 +1549,16 @@ window.RetryButton = RetryButton;
/**
* Appends a message to the in-character chat log.
- * @param {string} toadd the string to be added
+ * @param {string} msg the string to be added
* @param {string} name the name of the sender
*/
-function appendICLog(toadd, name = "", time = new Date()) {
+function appendICLog(msg, name = "", time = new Date()) {
const entry = document.createElement("p");
const nameField = document.createElement("span");
nameField.id = "iclog_name";
nameField.appendChild(document.createTextNode(name));
entry.appendChild(nameField);
- entry.appendChild(document.createTextNode(toadd));
+ entry.appendChild(document.createTextNode(msg));
// Only put a timestamp if the minute has changed.
if (lastICMessageTime.getMinutes() !== time.getMinutes()) {
@@ -997,7 +1574,8 @@ function appendICLog(toadd, name = "", time = new Date()) {
const clientLog = document.getElementById("client_log");
clientLog.appendChild(entry);
- if (clientLog.scrollTop > clientLog.scrollHeight - 600) {
+ /* This is a little buggy - some troubleshooting might be desirable */
+ if (clientLog.scrollTop > clientLog.scrollHeight - 800) {
clientLog.scrollTop = clientLog.scrollHeight;
}
@@ -1006,9 +1584,10 @@ function appendICLog(toadd, name = "", time = new Date()) {
/**
* Requests to play as a character.
- * @param {number} ccharacter the character ID; if this is a large number, then spectator is chosen instead.
+ * @param {number} ccharacter the character ID; if this is a large number,
+ * then spectator is chosen instead.
*/
-export function pickchar(ccharacter) {
+export function pickChar(ccharacter) {
if (ccharacter < 1000) {
client.sendCharacter(ccharacter);
} else {
@@ -1018,28 +1597,361 @@ export function pickchar(ccharacter) {
document.getElementById("client_emo").style.display = "none";
}
}
-window.pickchar = pickchar;
+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) {
- if (client.selectedEmote != -1) {
- document.getElementById("emo_" + client.selectedEmote).src = client.myEmote().button_off;
+export function pickEmotion(emo) {
+ if (client.selectedEmote !== -1) {
+ document.getElementById("emo_" + client.selectedEmote).src = client.emote.button_off;
+ }
+ client.selectedEmote = emo;
+ document.getElementById("emo_" + emo).src = client.emote.button_on;
+}
+window.pickEmotion = pickEmotion;
+
+/**
+ * Highlights and selects an evidence for in-character chat.
+ * @param {string} evidence the evidence to be presented
+ */
+export function pickEvidence(evidence) {
+ if (client.selectedEvidence !== evidence) {
+ //Update selected evidence
+ if (client.selectedEvidence > 0) {
+ document.getElementById("evi_" + client.selectedEvidence).className = "client_button";
+ }
+ document.getElementById("evi_" + evidence).className = "client_button dark";
+ client.selectedEvidence = evidence;
+
+ // Show evidence on information window
+ document.getElementById("evi_name").value = client.evidences[evidence - 1].name;
+ document.getElementById("evi_desc").value = client.evidences[evidence - 1].desc;
+
+ // Update icon
+ const icon_id = getIndexFromSelect("evi_select", client.evidences[evidence - 1].filename);
+ document.getElementById("evi_select").selectedIndex = icon_id;
+ if (icon_id === 0) {
+ document.getElementById("evi_filename").value = client.evidences[evidence - 1].filename;
+ }
+ updateEvidenceIcon();
+
+ // Update button
+ document.getElementById("evi_add").className = "client_button hover_button inactive";
+ document.getElementById("evi_edit").className = "client_button hover_button";
+ document.getElementById("evi_cancel").className = "client_button hover_button";
+ document.getElementById("evi_del").className = "client_button hover_button";
+ } else {
+ cancelEvidence();
+ }
+}
+window.pickEvidence = pickEvidence;
+
+/**
+ * Add evidence.
+ */
+export function addEvidence() {
+ const evidence_select = document.getElementById("evi_select");
+ client.sendPE(document.getElementById("evi_name").value,
+ document.getElementById("evi_desc").value,
+ evidence_select.selectedIndex === 0 ?
+ document.getElementById("evi_filename").value :
+ evidence_select.options[evidence_select.selectedIndex].text
+ );
+ cancelEvidence();
+}
+window.addEvidence = addEvidence;
+
+/**
+ * Edit selected evidence.
+ */
+export function editEvidence() {
+ const evidence_select = document.getElementById("evi_select");
+ const id = parseInt(client.selectedEvidence) - 1;
+ client.sendEE(id,
+ document.getElementById("evi_name").value,
+ document.getElementById("evi_desc").value,
+ evidence_select.selectedIndex === 0 ?
+ document.getElementById("evi_filename").value :
+ evidence_select.options[evidence_select.selectedIndex].text
+ );
+ cancelEvidence();
+}
+window.editEvidence = editEvidence;
+
+/**
+ * Delete selected evidence.
+ */
+export function deleteEvidence() {
+ const id = parseInt(client.selectedEvidence) - 1;
+ client.sendDE(id);
+ cancelEvidence();
+}
+window.deleteEvidence = deleteEvidence;
+
+/**
+ * Cancel evidence selection.
+ */
+export function cancelEvidence() {
+ //Clear evidence data
+ if (client.selectedEvidence > 0) {
+ document.getElementById("evi_" + client.selectedEvidence).className = "client_button";
+ }
+ client.selectedEvidence = 0;
+
+ // Clear evidence on information window
+ document.getElementById("evi_select").selectedIndex = 0;
+ updateEvidenceIcon(); // Update icon widget
+ document.getElementById("evi_filename").value = "";
+ document.getElementById("evi_name").value = "";
+ document.getElementById("evi_desc").value = "";
+ document.getElementById("evi_icon").style.backgroundImage = "url('misc/empty.png')"; //Clear icon
+
+ // Update button
+ document.getElementById("evi_add").className = "client_button hover_button";
+ document.getElementById("evi_edit").className = "client_button hover_button inactive";
+ document.getElementById("evi_cancel").className = "client_button hover_button inactive";
+ document.getElementById("evi_del").className = "client_button hover_button inactive";
+}
+window.cancelEvidence = cancelEvidence;
+
+/**
+ * Find index of anything in select box.
+ * @param {string} select_box the select element name
+ * @param {string} value the value that need to be compared
+ */
+export function getIndexFromSelect(select_box, value) {
+ //Find if icon alraedy existed in select box
+ const select_element = document.getElementById(select_box);
+ for (let i = 1; i < select_element.length; ++i) {
+ if (select_element.options[i].value === value) {
+ return i;
+ }
+ }
+ return 0;
+}
+window.getIndexFromSelect = getIndexFromSelect;
+
+/**
+ * Update evidence icon.
+ */
+export function updateEvidenceIcon() {
+ const evidence_select = document.getElementById("evi_select");
+ const evidence_filename = document.getElementById("evi_filename");
+ const evidence_iconbox = document.getElementById("evi_icon");
+
+ if (evidence_select.selectedIndex === 0) {
+ evidence_filename.style.display = "initial";
+ evidence_iconbox.style.backgroundImage = `url(${AO_HOST}evidence/${evidence_filename.value.toLowerCase()})`;
+ } else {
+ evidence_filename.style.display = "none";
+ evidence_iconbox.style.backgroundImage = `url(${AO_HOST}evidence/${evidence_select.value.toLowerCase()})`;
+ }
+}
+window.updateEvidenceIcon = updateEvidenceIcon;
+
+/**
+ * Update evidence icon.
+ */
+export function updateActionCommands(side) {
+ if (side === "jud") {
+ document.getElementById("judge_action").style.display = "inline-table";
+ document.getElementById("no_action").style.display = "none";
+ } else {
+ document.getElementById("judge_action").style.display = "none";
+ document.getElementById("no_action").style.display = "inline-table";
+ }
+
+ // Update role selector
+ for (let i = 0, role_select = document.getElementById("role_select").options; i < role_select.length; i++) {
+ if (side === role_select[i].value) {
+ role_select.selectedIndex = i;
+ return;
+ }
+ }
+}
+window.updateActionCommands = updateActionCommands;
+
+/**
+ * Change background via OOC.
+ */
+export function changeBackgroundOOC() {
+ const selectedBG = document.getElementById("bg_select");
+ const changeBGCommand = document.getElementById("bg_command").value;
+ const bgFilename = document.getElementById("bg_filename");
+
+ let filename = "";
+ if (selectedBG.selectedIndex === 0) {
+ filename = bgFilename.value;
+ } else {
+ filename = selectedBG.value;
}
- client.selectedEmote = emo
- document.getElementById("emo_" + emo).src = client.myEmote().button_on;
+ client.sendOOC("/" + changeBGCommand.replace("$1", filename));
}
-window.pickemotion = pickemotion;
+window.changeBackgroundOOC = changeBackgroundOOC;
+
+/**
+ * Change role via OOC.
+ */
+export function changeRoleOOC() {
+ const role_select = document.getElementById("role_select");
+ const role_command = document.getElementById("role_command").value;
+
+ client.sendOOC("/" + role_command.replace("$1", role_select.value));
+ updateActionCommands(role_select.value);
+}
+window.changeRoleOOC = changeRoleOOC;
+
+/**
+ * Random character via OOC.
+ */
+export function randomCharacterOOC() {
+ client.sendOOC("/" + document.getElementById("randomchar_command").value);
+}
+window.randomCharacterOOC = randomCharacterOOC;
+
+/**
+ * Call mod.
+ */
+export function callMod() {
+ $("#callmod_dialog").dialog("open");
+}
+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;
+
+/**
+ * Increment defense health point.
+ */
+export function addHPD() {
+ client.sendHP(1, String(parseInt(client.hp[0]) + 1));
+}
+window.addHPD = addHPD;
+
+/**
+ * Decrement defense health point.
+ */
+export function redHPD() {
+ client.sendHP(1, String(parseInt(client.hp[0]) - 1));
+}
+window.redHPD = redHPD;
+
+/**
+ * Increment prosecution health point.
+ */
+export function addHPP() {
+ client.sendHP(2, String(parseInt(client.hp[1]) + 1));
+}
+window.addHPP = addHPP;
+
+/**
+ * Decrement prosecution health point.
+ */
+export function redHPP() {
+ client.sendHP(2, String(parseInt(client.hp[1]) - 1));
+}
+window.redHPP = redHPP;
+
+/**
+ * Update background preview.
+ */
+export function updateBackgroundPreview() {
+ const background_select = document.getElementById("bg_select");
+ const background_filename = document.getElementById("bg_filename");
+ const background_preview = document.getElementById("bg_preview");
+
+ if (background_select.selectedIndex === 0) {
+ background_filename.style.display = "initial";
+ background_preview.src = AO_HOST + "background/" + background_filename.value.toLowerCase() + "/defenseempty.png";
+ } else {
+ background_filename.style.display = "none";
+ background_preview.src = AO_HOST + "background/" + background_select.value.toLowerCase() + "/defenseempty.png";
+ }
+}
+window.updateBackgroundPreview = updateBackgroundPreview;
+
+/**
+ * Highlights and selects an effect for in-character chat.
+ * If the same effect button is selected, then the effect is canceled.
+ * @param {string} effect the new effect to be selected
+ */
+export function toggleEffect(effect) {
+ if (effect === selectedEffect) {
+ document.getElementById("button_effect_" + effect).className = "client_button";
+ selectedEffect = 0;
+ } else {
+ document.getElementById("button_effect_" + effect).className = "client_button dark";
+ if (selectedEffect) {
+ document.getElementById("button_effect_" + selectedEffect).className = "client_button";
+ }
+ selectedEffect = effect;
+ }
+}
+window.toggleEffect = toggleEffect;
+
+/**
+ * Toggle flip for in-character chat.
+ */
+export function toggleFlip() {
+ if (client.flip) {
+ document.getElementById("button_flip").className = "client_button";
+ } else {
+ document.getElementById("button_flip").className = "client_button dark";
+ }
+ client.flip = !client.flip;
+}
+window.toggleFlip = toggleFlip;
+
+/**
+ * Toggle presentable for presenting evidence in-character chat.
+ */
+export function togglePresent() {
+ if (client.presentable) {
+ document.getElementById("button_present").className = "client_button";
+ } else {
+ document.getElementById("button_present").className = "client_button dark";
+ }
+ client.presentable = !client.presentable;
+}
+window.togglePresent = togglePresent;
+
+/**
+ * Highlights and selects a menu.
+ * @param {string} menu the menu to be selected
+ */
+export function toggleMenu(menu) {
+ if (menu !== selectedMenu) {
+ document.getElementById("menu_" + menu).className = "menu_icon active";
+ document.getElementById("content_" + menu).className = "menu_content active";
+ document.getElementById("menu_" + selectedMenu).className = "menu_icon";
+ document.getElementById("content_" + selectedMenu).className = "menu_content";
+ selectedMenu = menu;
+ }
+}
+window.toggleMenu = toggleMenu;
/**
* Highlights and selects a shout for in-character chat.
* If the same shout button is selected, then the shout is canceled.
* @param {string} shout the new shout to be selected
*/
-export function toggleshout(shout) {
- if (shout == selectedShout) {
+export function toggleShout(shout) {
+ if (shout === selectedShout) {
document.getElementById("button_" + shout).className = "client_button";
selectedShout = 0;
} else {
@@ -1050,7 +1962,7 @@ export function toggleshout(shout) {
selectedShout = shout;
}
}
-window.toggleshout = toggleshout;
+window.toggleShout = toggleShout;
/**
* Escapes a string to be HTML-safe.
@@ -1079,27 +1991,80 @@ function escapeChat(estring) {
.replace(/\$/g, "<dollar>");
}
-// TODO: Possibly safe to remove, since we are using a transpiler.
-if (typeof(String.prototype.trim) === "undefined")
-{
- String.prototype.trim = function()
- {
- return String(this).replace(/^\s+|\s+$/g, '');
- };
+/**
+ * Unescapes a string to AO1 escape codes.
+ * @param {string} estring the string to be unescaped
+ */
+function unescapeChat(estring) {
+ return estring
+ .replace(/<pound>/g, "#")
+ .replace(/<and>/g, "&")
+ .replace(/<percent>/g, "%")
+ .replace(/<dollar>/g, "$");
+}
+
+/**
+ * Encode text on client side.
+ * @param {string} estring the string to be encoded
+ */
+function encodeChat(estring) {
+ const selectedEncoding = document.getElementById("client_encoding").value;
+ if (selectedEncoding === "unicode") {
+ // This approach works by escaping all special characters to Unicode escape sequences.
+ // Source: https://gist.github.com/mathiasbynens/1243213
+ return estring.replace(/[^\0-~]/g, function (ch) {
+ return "\\u" + ("000" + ch.charCodeAt().toString(16)).slice(-4);
+ });
+ } else if (selectedEncoding === "utf16") {
+ // Source: https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
+ const buffer = new ArrayBuffer(estring.length * 2);
+ const result = new Uint16Array(buffer);
+ for (let i = 0, strLen = estring.length; i < strLen; i++) {
+ result[i] = estring.charCodeAt(i);
+ }
+ return String(result);
+ } else {
+ return estring;
+ }
}
-// Used for HDID calculation.
-String.prototype.hashCode = function() {
- var hash = 0, i, chr;
- if (this.length === 0) return hash;
- for (i = 0; i < this.length; i++) {
- chr = this.charCodeAt(i);
- hash = ((hash << 5) - hash) + chr;
- hash |= 0; // Convert to 32bit integer
+/**
+ * Decodes text on client side.
+ * @param {string} estring the string to be decoded
+ */
+function decodeChat(estring) {
+ const selectedDecoding = document.getElementById("client_decoding").value;
+ if (selectedDecoding === "unicode") {
+ // Source: https://stackoverflow.com/questions/7885096/how-do-i-decode-a-string-with-escaped-unicode
+ return estring.replace(/\\u([\d\w]{1,})/gi, function (match, group) {
+ return String.fromCharCode(parseInt(group, 16));
+ });
+ } else if (selectedDecoding === "utf16") {
+ // Source: https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
+ return String.fromCharCode.apply(null, new Uint16Array(estring.split(",")));
+ } else {
+ return estring;
}
- return hash;
-};
+}
+/**
+ * Decoding text on client side.
+ * @param {string} estring the string to be decoded
+ */
+function decodeBBCode(estring) {
+ return estring
+ .replace(/\\n/g, "<br>") // Newline \n
+ .replace(/\[(\/?)b\]/g, "<$1b>") // Bold [b][/b]
+ .replace(/\[(\/?)i\]/g, "<$1i>") // Italic [i][/i]
+ .replace(/\[(\/?)s\]/g, "<$1del>") // Strikethrough [s][/s]
+ .replace(/\[(\/?)u\]/g, "<$1u>") // Underline [u][/u]
+ .replace(/\[(\/?)sub\]/g, "<$1sub>") // Subscript [sub][/sub]
+ .replace(/\[(\/?)sup\]/g, "<$1sup>") // Superscript [sup][/sup]
+ .replace(/\[m=([#a-zA-Z0-9]+)\]/g, "<m a=\"$1\">") // Markup [m=#0ff]
+ .replace(/\[(\/?)m\]/g, "<$1m>") // [m][/m]
+ .replace(/\[c=?([#a-zA-Z0-9]+)\]/g, "<c a=\"$1\">") // Color [c=red]
+ .replace(/\[\/c\]/g, "</c>"); // [/c]
+}
//
// Client code
@@ -1107,3 +2072,39 @@ String.prototype.hashCode = function() {
let client = new Client(serverIP);
let viewport = new Viewport();
+
+$(document).ready(function () {
+ client.initialObservBBCode();
+ client.loadResources();
+});
+
+// Create dialog and link to button
+$(function () {
+ $("#callmod_dialog").dialog({
+ autoOpen: false,
+ resizable: false,
+ show: {
+ effect: "drop",
+ direction: "down",
+ duration: 500
+ },
+ hide: {
+ effect: "drop",
+ direction: "down",
+ duration: 500
+ },
+ height: "auto",
+ width: 400,
+ modal: true,
+ buttons: {
+ Sure: function () {
+ const reason = prompt("Please enter the reason", "");
+ client.sendZZ(reason);
+ $(this).dialog("close");
+ },
+ Cancel: function () {
+ $(this).dialog("close");
+ }
+ }
+ });
+});