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