aboutsummaryrefslogtreecommitdiff
path: root/webAO/viewport/viewport.ts
diff options
context:
space:
mode:
authorCaleb Mabry <36182383+caleb-mabry@users.noreply.github.com>2023-02-01 15:54:26 -0500
committerGitHub <noreply@github.com>2023-02-01 15:54:26 -0500
commit39edb1077725a6b9db58b4c32323e847ece5f2f6 (patch)
tree2ab4e9d3c0a007684cb3fb706f70fc677e0f7eca /webAO/viewport/viewport.ts
parent0eabb5da1759439fd01e54dc986900448f483330 (diff)
parentf8ea065b5e6bccbebb918293e13cab4adb536b70 (diff)
Merge branch 'master' into fix-pipeline
Diffstat (limited to 'webAO/viewport/viewport.ts')
-rw-r--r--webAO/viewport/viewport.ts495
1 files changed, 495 insertions, 0 deletions
diff --git a/webAO/viewport/viewport.ts b/webAO/viewport/viewport.ts
new file mode 100644
index 0000000..de95030
--- /dev/null
+++ b/webAO/viewport/viewport.ts
@@ -0,0 +1,495 @@
+import { client, delay } from "../client";
+import { UPDATE_INTERVAL } from "../client";
+import setEmote from "../client/setEmote";
+import { safeTags } from "../encoding";
+import { AO_HOST } from "../client/aoHost";
+import { Viewport } from './interfaces/Viewport'
+import { createBlipsChannels } from './utils/createBlipChannels'
+import { defaultChatMsg } from './constants/defaultChatMsg'
+import { createMusic } from './utils/createMusic'
+import { createSfxAudio } from './utils/createSfxAudio'
+import { createShoutAudio } from './utils/createShoutAudio'
+import { createTestimonyAudio } from './utils/createTestimonyAudio'
+import { Testimony } from './interfaces/Testimony'
+import { COLORS } from './constants/colors'
+import { set_side } from './utils/setSide'
+import { ChatMsg } from "./interfaces/ChatMsg";
+import { setStartFirstTickCheck, setStartSecondTickCheck, startFirstTickCheck, startSecondTickCheck } from "./utils/handleICSpeaking";
+
+const viewport = (): Viewport => {
+ let animating = false;
+ let blipChannels = createBlipsChannels();
+ let chatmsg = defaultChatMsg;
+ let currentBlipChannel = 0;
+ let lastChar = "";
+ let lastEvi = 0;
+ let music = createMusic();
+ let musicVolume = 0;
+ let sfxAudio = createSfxAudio();
+ let sfxplayed = 0;
+ let shoutTimer = 0;
+ let shoutaudio = createShoutAudio();
+ let testimonyAudio = createTestimonyAudio();
+ let testimonyTimer = 0;
+ let testimonyUpdater: any;
+ let textnow = "";
+ let theme: string;
+ let tickTimer = 0;
+ let updater: any;
+ let backgroundName = "";
+ const getSfxAudio = () => sfxAudio;
+ const setSfxAudio = (value: HTMLAudioElement) => { sfxAudio = value };
+ const getBackgroundName = () => backgroundName;
+ const setBackgroundName = (value: string) => { backgroundName = value };
+ const getBackgroundFolder = () =>
+ `${AO_HOST}background/${encodeURI(backgroundName.toLowerCase())}/`;
+ const getTextNow = () => {return textnow}
+ const setTextNow = (val: string) => {textnow = val}
+ const getChatmsg = () => {return chatmsg}
+ const setChatmsg = (val: ChatMsg) => {chatmsg = val}
+ const getSfxPlayed = () => sfxplayed
+ const setSfxPlayed = (val: number) => {sfxplayed = val}
+ const getTickTimer = () => tickTimer
+ const setTickTimer = (val: number) => {tickTimer = val}
+ const getAnimating = () => animating
+ const setAnimating = (val: boolean) => {animating = val}
+ const getLastEvidence = () => lastEvi
+ const setLastEvidence = (val: number) => {lastEvi = val}
+ const setLastCharacter = (val: string) => {lastChar = val}
+ const getLastCharacter = () => lastChar
+ const getShoutTimer = () => shoutTimer
+ const setShoutTimer = (val: number) => {shoutTimer = val}
+ const getTheme = () => theme
+ const setTheme = (val: string) => {theme = val}
+ const getTestimonyTimer = () => testimonyTimer;
+ const setTestimonyTimer = (val: number) => {testimonyTimer = val}
+ const setTestimonyUpdater = (val: any) => {testimonyUpdater = val}
+ const getTestimonyUpdater = () => testimonyUpdater
+ const playSFX = async (sfxname: string, looping: boolean) => {
+ sfxAudio.pause();
+ sfxAudio.loop = looping;
+ sfxAudio.src = sfxname;
+ sfxAudio.play();
+ };
+ /**
+ * Updates the testimony overaly
+ */
+ const updateTestimony = () => {
+ const testimonyFilenames: Testimony = {
+ 1: "witnesstestimony",
+ 2: "crossexamination",
+ 3: "notguilty",
+ 4: "guilty",
+ };
+
+ // Update timer
+ testimonyTimer += UPDATE_INTERVAL;
+
+ const testimony = testimonyFilenames[client.testimonyID];
+ const resource = client.resources[testimony];
+ if (!resource) {
+ disposeTestimony();
+ return;
+ }
+
+ if (testimonyTimer >= resource.duration) {
+ disposeTestimony();
+ } else {
+ testimonyUpdater = setTimeout(() => updateTestimony(), UPDATE_INTERVAL);
+ }
+ };
+ /**
+ * Dispose the testimony overlay
+ */
+ const disposeTestimony = () => {
+ client.testimonyID = 0;
+ testimonyTimer = 0;
+ document.getElementById("client_testimony").style.opacity = "0";
+ clearTimeout(testimonyUpdater);
+ };
+ const handleTextTick = async (charLayers: HTMLImageElement) => {
+ const chatBox = document.getElementById("client_chat");
+ const waitingBox = document.getElementById("client_chatwaiting");
+ const chatBoxInner = document.getElementById("client_inner_chat");
+ const charName = chatmsg.name.toLowerCase();
+ const charEmote = chatmsg.sprite.toLowerCase();
+
+ if (chatmsg.content.charAt(textnow.length) !== " ") {
+ blipChannels[currentBlipChannel].play();
+ currentBlipChannel++;
+ currentBlipChannel %= blipChannels.length;
+ }
+ textnow = chatmsg.content.substring(0, textnow.length + 1);
+ const characterElement = chatmsg.parsed[textnow.length - 1];
+ if (characterElement) {
+ const COMMAND_IDENTIFIER = "\\";
+
+ const nextCharacterElement = chatmsg.parsed[textnow.length];
+ const flash = async () => {
+ const effectlayer = document.getElementById("client_fg");
+ playSFX(`${AO_HOST}sounds/general/sfx-realization.opus`, false);
+ effectlayer.style.animation = "flash 0.4s 1";
+ await delay(400);
+ effectlayer.style.removeProperty("animation");
+ };
+
+ const shake = async () => {
+ const gamewindow = document.getElementById("client_gamewindow");
+ playSFX(`${AO_HOST}sounds/general/sfx-stab.opus`, false);
+ gamewindow.style.animation = "shake 0.2s 1";
+ await delay(200);
+ gamewindow.style.removeProperty("animation");
+ };
+
+ const commands = new Map(
+ Object.entries({
+ s: shake,
+ f: flash,
+ })
+ );
+ const textSpeeds = new Set(["{", "}"]);
+
+ // Changing Text Speed
+ if (textSpeeds.has(characterElement.innerHTML)) {
+ // Grab them all in a row
+ const MAX_SLOW_CHATSPEED = 120;
+ for (let i = textnow.length; i < chatmsg.content.length; i++) {
+ const currentCharacter = chatmsg.parsed[i - 1].innerHTML;
+ if (currentCharacter === "}") {
+ if (chatmsg.speed > 0) {
+ chatmsg.speed -= 20;
+ }
+ } else if (currentCharacter === "{") {
+ if (chatmsg.speed < MAX_SLOW_CHATSPEED) {
+ chatmsg.speed += 20;
+ }
+ } else {
+ // No longer at a speed character
+ textnow = chatmsg.content.substring(0, i);
+ break;
+ }
+ }
+ }
+
+ if (
+ characterElement.innerHTML === COMMAND_IDENTIFIER &&
+ commands.has(nextCharacterElement?.innerHTML)
+ ) {
+ textnow = chatmsg.content.substring(0, textnow.length + 1);
+ await commands.get(nextCharacterElement.innerHTML)();
+ } else {
+ chatBoxInner.appendChild(chatmsg.parsed[textnow.length - 1]);
+ }
+ }
+ // scroll to bottom
+ chatBox.scrollTop = chatBox.scrollHeight;
+
+ if (textnow === chatmsg.content) {
+ animating = false;
+ setEmote(
+ AO_HOST,
+ client,
+ charName,
+ charEmote,
+ "(a)",
+ false,
+ chatmsg.side
+ );
+ charLayers.style.opacity = "1";
+ waitingBox.style.opacity = "1";
+ clearTimeout(updater);
+ }
+ };
+ /**
+ * Updates the chatbox based on the given text.
+ *
+ * OK, here's the documentation on how this works:
+ *
+ * 1 _animating
+ * If we're not done with this characters animation, i.e. his text isn't fully there, set a timeout for the next tick/step to happen
+ *
+ * 2 startpreanim
+ * If the shout timer is over it starts with the preanim
+ * The first thing it checks for is the shake effect (TODO on client this is handled by the @ symbol and not a flag )
+ * Then is the flash/realization effect
+ * After that, the shout image set to be transparent
+ * and the main characters preanim gif is loaded
+ * If pairing is supported the paired character will just stand around with his idle sprite
+ *
+ * 3 preanimdelay over
+ * this animates the evidence popup and finally shows the character name and message box
+ * it sets the text color and the character speaking sprite
+ *
+ * 4 textnow != content
+ * this adds a character to the textbox and stops the animations if the entire message is present in the textbox
+ *
+ * 5 sfx
+ * independent of the stuff above, this will play any sound effects specified by the emote the character sent.
+ * happens after the shout delay + an sfx delay that comes with the message packet
+ *
+ * XXX: This relies on a global variable `chatmsg`!
+ */
+ const chat_tick = async () => {
+ // note: this is called fairly often
+ // do not perform heavy operations here
+ await delay(chatmsg.speed);
+ if (textnow === chatmsg.content) {
+ return;
+ }
+
+ const gamewindow = document.getElementById("client_gamewindow");
+ const waitingBox = document.getElementById("client_chatwaiting");
+ const eviBox = <HTMLImageElement>document.getElementById("client_evi");
+ const shoutSprite = <HTMLImageElement>(
+ document.getElementById("client_shout")
+ );
+ const effectlayer = <HTMLImageElement>document.getElementById("client_fg");
+ const chatBoxInner = document.getElementById("client_inner_chat");
+ let charLayers = <HTMLImageElement>document.getElementById("client_char");
+ let pairLayers = <HTMLImageElement>(
+ document.getElementById("client_pair_char")
+ );
+
+ const validSides: string[] = ["def", "pro", "wit"]; // these are for the full view pan, the other positions use 'client_char'
+ if (validSides.includes(chatmsg.side)) {
+ charLayers = <HTMLImageElement>(
+ document.getElementById(`client_${chatmsg.side}_char`)
+ );
+ pairLayers = <HTMLImageElement>(
+ document.getElementById(`client_${chatmsg.side}_pair_char`)
+ );
+ }
+
+ const charName = chatmsg.name.toLowerCase();
+ const charEmote = chatmsg.sprite.toLowerCase();
+
+ const pairName = chatmsg.other_name.toLowerCase();
+ const pairEmote = chatmsg.other_emote.toLowerCase();
+
+ // TODO: preanims sometimes play when they're not supposed to
+ const isShoutOver = tickTimer >= shoutTimer;
+ const isShoutAndPreanimOver =
+ tickTimer >= shoutTimer + chatmsg.preanimdelay;
+ if (isShoutOver && startFirstTickCheck) {
+ // Effect stuff
+ if (chatmsg.screenshake === 1) {
+ // Shake screen
+ playSFX(`${AO_HOST}sounds/general/sfx-stab.opus`, false);
+ gamewindow.style.animation = "shake 0.2s 1";
+ }
+ if (chatmsg.flash === 1) {
+ // Flash screen
+ playSFX(`${AO_HOST}sounds/general/sfx-realization.opus`, false);
+ effectlayer.style.animation = "flash 0.4s 1";
+ }
+
+ // Pre-animation stuff
+ if (chatmsg.preanimdelay > 0) {
+ shoutSprite.style.opacity = "0";
+ shoutSprite.style.animation = "";
+ const preanim = chatmsg.preanim.toLowerCase();
+ setEmote(AO_HOST, client, charName, preanim, "", false, chatmsg.side);
+ }
+
+ if (chatmsg.other_name) {
+ pairLayers.style.opacity = "1";
+ } else {
+ pairLayers.style.opacity = "0";
+ }
+
+ // Done with first check, move to second
+ setStartFirstTickCheck(false)
+ setStartSecondTickCheck(true)
+
+ chatmsg.startpreanim = false;
+ chatmsg.startspeaking = true;
+ }
+
+ const hasNonInterruptingPreAnim = chatmsg.noninterrupting_preanim === 1;
+ if (textnow !== chatmsg.content && hasNonInterruptingPreAnim) {
+ const chatContainerBox = document.getElementById("client_chatcontainer");
+ chatContainerBox.style.opacity = "1";
+ await handleTextTick(charLayers);
+ } else if (isShoutAndPreanimOver && startSecondTickCheck) {
+ if (chatmsg.startspeaking) {
+ chatmsg.startspeaking = false;
+
+ // Evidence Bullshit
+ if (chatmsg.evidence > 0) {
+ // Prepare evidence
+ eviBox.src = safeTags(
+ client.evidences[chatmsg.evidence - 1].icon
+ );
+
+ eviBox.style.width = "auto";
+ eviBox.style.height = "36.5%";
+ eviBox.style.opacity = "1";
+
+ testimonyAudio.src = `${AO_HOST}sounds/general/sfx-evidenceshoop.opus`;
+ testimonyAudio.play();
+
+ if (chatmsg.side === "def") {
+ // Only def show evidence on right
+ eviBox.style.right = "1em";
+ eviBox.style.left = "initial";
+ } else {
+ eviBox.style.right = "initial";
+ eviBox.style.left = "1em";
+ }
+ }
+ chatBoxInner.className = `text_${COLORS[chatmsg.color]}`;
+
+ if (chatmsg.preanimdelay === 0) {
+ shoutSprite.style.opacity = "0";
+ shoutSprite.style.animation = "";
+ }
+
+ switch (Number(chatmsg.deskmod)) {
+ case 2:
+ set_side({
+ position: chatmsg.side,
+ showSpeedLines: false,
+ showDesk: true,
+ });
+ break;
+ case 3:
+ set_side({
+ position: chatmsg.side,
+ showSpeedLines: false,
+ showDesk: false,
+ });
+ break;
+ case 4:
+ set_side({
+ position: chatmsg.side,
+ showSpeedLines: false,
+ showDesk: true,
+ });
+ break;
+ case 5:
+ set_side({
+ position: chatmsg.side,
+ showSpeedLines: false,
+ showDesk: false,
+ });
+ break;
+ }
+
+ if (chatmsg.other_name) {
+ setEmote(
+ AO_HOST,
+ client,
+ pairName,
+ pairEmote,
+ "(a)",
+ true,
+ chatmsg.side
+ );
+ pairLayers.style.opacity = "1";
+ } else {
+ pairLayers.style.opacity = "0";
+ }
+
+ setEmote(
+ AO_HOST,
+ client,
+ charName,
+ charEmote,
+ "(b)",
+ false,
+ chatmsg.side
+ );
+ charLayers.style.opacity = "1";
+
+ if (textnow === chatmsg.content) {
+ setEmote(
+ AO_HOST,
+ client,
+ charName,
+ charEmote,
+ "(a)",
+ false,
+ chatmsg.side
+ );
+ charLayers.style.opacity = "1";
+ waitingBox.style.opacity = "1";
+ animating = false;
+ clearTimeout(updater);
+ return;
+ }
+ } else if (textnow !== chatmsg.content) {
+ const chatContainerBox = document.getElementById(
+ "client_chatcontainer"
+ );
+ chatContainerBox.style.opacity = "1";
+ await handleTextTick(charLayers);
+ }
+ }
+
+ if (!sfxplayed && chatmsg.snddelay + shoutTimer >= tickTimer) {
+ sfxplayed = 1;
+ if (
+ chatmsg.sound !== "0" &&
+ chatmsg.sound !== "1" &&
+ chatmsg.sound !== "" &&
+ chatmsg.sound !== undefined &&
+ (chatmsg.type == 1 || chatmsg.type == 2 || chatmsg.type == 6)
+ ) {
+ playSFX(
+ `${AO_HOST}sounds/general/${encodeURI(
+ chatmsg.sound.toLowerCase()
+ )}.opus`,
+ chatmsg.looping_sfx
+ );
+ }
+ }
+ if (animating) {
+ chat_tick();
+ }
+ tickTimer += UPDATE_INTERVAL;
+ };
+
+ return {
+ getTextNow,
+ setTextNow,
+ getChatmsg,
+ setChatmsg,
+ getSfxPlayed,
+ setSfxPlayed,
+ setTickTimer,
+ getTickTimer,
+ setAnimating,
+ getAnimating,
+ getLastEvidence,
+ setLastEvidence,
+ setLastCharacter,
+ getLastCharacter,
+ getShoutTimer,
+ setShoutTimer,
+ setTheme,
+ getTheme,
+ setTestimonyTimer,
+ getTestimonyTimer,
+ setTestimonyUpdater,
+ getTestimonyUpdater,
+ testimonyAudio,
+ chat_tick,
+ playSFX,
+ set_side,
+ setBackgroundName,
+ updateTestimony,
+ disposeTestimony,
+ handleTextTick,
+ getBackgroundFolder,
+ getBackgroundName,
+ getSfxAudio,
+ setSfxAudio,
+ blipChannels,
+ music,
+ musicVolume,
+ shoutaudio,
+ updater,
+ };
+};
+
+export default viewport;