diff options
Diffstat (limited to 'webAO/viewport')
| -rw-r--r-- | webAO/viewport/constants/colors.ts | 12 | ||||
| -rw-r--r-- | webAO/viewport/constants/defaultChatMsg.ts | 15 | ||||
| -rw-r--r-- | webAO/viewport/constants/positions.ts | 45 | ||||
| -rw-r--r-- | webAO/viewport/constants/shouts.ts | 1 | ||||
| -rw-r--r-- | webAO/viewport/interfaces/ChatMsg.ts | 34 | ||||
| -rw-r--r-- | webAO/viewport/interfaces/Desk.ts | 4 | ||||
| -rw-r--r-- | webAO/viewport/interfaces/Position.ts | 7 | ||||
| -rw-r--r-- | webAO/viewport/interfaces/Positions.ts | 5 | ||||
| -rw-r--r-- | webAO/viewport/interfaces/Testimony.ts | 3 | ||||
| -rw-r--r-- | webAO/viewport/interfaces/Viewport.ts | 43 | ||||
| -rw-r--r-- | webAO/viewport/utils/createBlipChannels.ts | 15 | ||||
| -rw-r--r-- | webAO/viewport/utils/createMusic.ts | 13 | ||||
| -rw-r--r-- | webAO/viewport/utils/createSfxAudio.ts | 9 | ||||
| -rw-r--r-- | webAO/viewport/utils/createShoutAudio.ts | 9 | ||||
| -rw-r--r-- | webAO/viewport/utils/createTestimonyAudio.ts | 9 | ||||
| -rw-r--r-- | webAO/viewport/utils/handleICSpeaking.ts | 311 | ||||
| -rw-r--r-- | webAO/viewport/utils/initTestimonyUpdater.ts | 31 | ||||
| -rw-r--r-- | webAO/viewport/utils/setSide.ts | 90 | ||||
| -rw-r--r-- | webAO/viewport/viewport.ts | 495 |
19 files changed, 1151 insertions, 0 deletions
diff --git a/webAO/viewport/constants/colors.ts b/webAO/viewport/constants/colors.ts new file mode 100644 index 0000000..4858081 --- /dev/null +++ b/webAO/viewport/constants/colors.ts @@ -0,0 +1,12 @@ +export const COLORS = [ + "white", + "green", + "red", + "orange", + "blue", + "yellow", + "pink", + "cyan", + "grey", + "rainbow", + ];
\ No newline at end of file diff --git a/webAO/viewport/constants/defaultChatMsg.ts b/webAO/viewport/constants/defaultChatMsg.ts new file mode 100644 index 0000000..8a5db6b --- /dev/null +++ b/webAO/viewport/constants/defaultChatMsg.ts @@ -0,0 +1,15 @@ +import { UPDATE_INTERVAL } from "../../client"; +import { ChatMsg } from "../interfaces/ChatMsg"; + +export const defaultChatMsg = { + content: "", + objection: 0, + sound: "", + startpreanim: true, + startspeaking: false, + side: null, + color: 0, + snddelay: 0, + preanimdelay: 0, + speed: UPDATE_INTERVAL, + } as ChatMsg;
\ No newline at end of file diff --git a/webAO/viewport/constants/positions.ts b/webAO/viewport/constants/positions.ts new file mode 100644 index 0000000..1712ac6 --- /dev/null +++ b/webAO/viewport/constants/positions.ts @@ -0,0 +1,45 @@ +import { Positions } from '../interfaces/Positions' +import { Desk } from '../interfaces/Desk'; + +export const positions: Positions = { + def: { + bg: "defenseempty", + desk: { ao2: "defensedesk.png", ao1: "bancodefensa.png" } as Desk, + speedLines: "defense_speedlines.gif", + }, + pro: { + bg: "prosecutorempty", + desk: { ao2: "prosecutiondesk.png", ao1: "bancoacusacion.png" } as Desk, + speedLines: "prosecution_speedlines.gif", + }, + hld: { + bg: "helperstand", + desk: {} as Desk, + speedLines: "defense_speedlines.gif", + }, + hlp: { + bg: "prohelperstand", + desk: {} as Desk, + speedLines: "prosecution_speedlines.gif", + }, + wit: { + bg: "witnessempty", + desk: { ao2: "stand.png", ao1: "estrado.png" } as Desk, + speedLines: "prosecution_speedlines.gif", + }, + jud: { + bg: "judgestand", + desk: { ao2: "judgedesk.png", ao1: "judgedesk.gif" } as Desk, + speedLines: "prosecution_speedlines.gif", + }, + jur: { + bg: "jurystand", + desk: { ao2: "jurydesk.png", ao1: "estrado.png" } as Desk, + speedLines: "defense_speedlines.gif", + }, + sea: { + bg: "seancestand", + desk: { ao2: "seancedesk.png", ao1: "estrado.png" } as Desk, + speedLines: "prosecution_speedlines.gif", + }, +};
\ No newline at end of file diff --git a/webAO/viewport/constants/shouts.ts b/webAO/viewport/constants/shouts.ts new file mode 100644 index 0000000..eddd6d3 --- /dev/null +++ b/webAO/viewport/constants/shouts.ts @@ -0,0 +1 @@ +export const SHOUTS = [undefined, "holdit", "objection", "takethat", "custom"]; diff --git a/webAO/viewport/interfaces/ChatMsg.ts b/webAO/viewport/interfaces/ChatMsg.ts new file mode 100644 index 0000000..6b96c6e --- /dev/null +++ b/webAO/viewport/interfaces/ChatMsg.ts @@ -0,0 +1,34 @@ +export interface ChatMsg { + content: string; + objection: number; + sound: string; + startpreanim?: boolean; + startspeaking?: boolean; + side: any; + color: number; + snddelay: number; + preanimdelay?: number; + speed: number; + blips: string; + self_offset?: number[]; + other_offset?: number[]; + showname?: string; + nameplate?: string; + flip?: number; + other_flip?: number; + effects?: string[]; + deskmod?: number; + preanim?: string; + other_name?: string; + sprite?: string; + name?: string; + chatbox?: string; + other_emote?: string; + parsed?: HTMLSpanElement[]; + screenshake?: number; + flash?: number; + type?: number; + evidence?: number; + looping_sfx?: boolean; + noninterrupting_preanim?: number; + }
\ No newline at end of file diff --git a/webAO/viewport/interfaces/Desk.ts b/webAO/viewport/interfaces/Desk.ts new file mode 100644 index 0000000..872426a --- /dev/null +++ b/webAO/viewport/interfaces/Desk.ts @@ -0,0 +1,4 @@ +export interface Desk { + ao2?: string; + ao1?: string; +}
\ No newline at end of file diff --git a/webAO/viewport/interfaces/Position.ts b/webAO/viewport/interfaces/Position.ts new file mode 100644 index 0000000..dea7238 --- /dev/null +++ b/webAO/viewport/interfaces/Position.ts @@ -0,0 +1,7 @@ +import { Desk } from './Desk' + +export interface Position { + bg?: string; + desk?: Desk; + speedLines: string; +}
\ No newline at end of file diff --git a/webAO/viewport/interfaces/Positions.ts b/webAO/viewport/interfaces/Positions.ts new file mode 100644 index 0000000..0644962 --- /dev/null +++ b/webAO/viewport/interfaces/Positions.ts @@ -0,0 +1,5 @@ +import { Position } from './Position' + +export interface Positions { + [key: string]: Position; +}
\ No newline at end of file diff --git a/webAO/viewport/interfaces/Testimony.ts b/webAO/viewport/interfaces/Testimony.ts new file mode 100644 index 0000000..61a7491 --- /dev/null +++ b/webAO/viewport/interfaces/Testimony.ts @@ -0,0 +1,3 @@ +export interface Testimony { + [key: number]: string; +}
\ No newline at end of file diff --git a/webAO/viewport/interfaces/Viewport.ts b/webAO/viewport/interfaces/Viewport.ts new file mode 100644 index 0000000..5b428c1 --- /dev/null +++ b/webAO/viewport/interfaces/Viewport.ts @@ -0,0 +1,43 @@ +import { ChatMsg } from "./ChatMsg"; + +export interface Viewport { + getTextNow: Function; + setTextNow: Function; + getChatmsg: Function; + setChatmsg: Function; + getSfxPlayed: Function; + setSfxPlayed: Function; + setTickTimer: Function; + getTickTimer: Function; + getAnimating: Function; + setAnimating: Function; + getLastEvidence: Function; + setLastEvidence: Function; + setLastCharacter: Function; + getLastCharacter: Function; + setShoutTimer: Function; + getShoutTimer: Function; + setTestimonyTimer: Function; + getTestimonyTimer: Function; + setTestimonyUpdater: Function; + getTestimonyUpdater: Function; + getTheme: Function; + setTheme: Function; + testimonyAudio: HTMLAudioElement; + chat_tick: Function; + playSFX: Function; + set_side: Function; + updateTestimony: Function; + disposeTestimony: Function; + handleTextTick: Function; + setSfxAudio: Function; + getSfxAudio: Function; + getBackgroundFolder: Function; + blipChannels: HTMLAudioElement[]; + music: any; + musicVolume: number; + setBackgroundName: Function; + getBackgroundName: Function; + shoutaudio: HTMLAudioElement; + updater: any; +} diff --git a/webAO/viewport/utils/createBlipChannels.ts b/webAO/viewport/utils/createBlipChannels.ts new file mode 100644 index 0000000..6296b3b --- /dev/null +++ b/webAO/viewport/utils/createBlipChannels.ts @@ -0,0 +1,15 @@ +import { opusCheck } from "../../dom/opusCheck"; + +export const createBlipsChannels = () => { + const blipSelectors = document.getElementsByClassName( + "blipSound" + ) as HTMLCollectionOf<HTMLAudioElement>; + + const blipChannels = [...blipSelectors]; + // Allocate multiple blip audio channels to make blips less jittery + blipChannels.forEach((channel: HTMLAudioElement) => (channel.volume = 0.5)); + blipChannels.forEach( + (channel: HTMLAudioElement) => (channel.onerror = opusCheck(channel)) + ); + return blipChannels; +};
\ No newline at end of file diff --git a/webAO/viewport/utils/createMusic.ts b/webAO/viewport/utils/createMusic.ts new file mode 100644 index 0000000..9bf5240 --- /dev/null +++ b/webAO/viewport/utils/createMusic.ts @@ -0,0 +1,13 @@ +import { opusCheck } from '../../dom/opusCheck' + +export const createMusic = () => { + const audioChannels = document.getElementsByClassName( + "audioChannel" + ) as HTMLCollectionOf<HTMLAudioElement>; + let music = [...audioChannels]; + music.forEach((channel: HTMLAudioElement) => (channel.volume = 0.5)); + music.forEach( + (channel: HTMLAudioElement) => (channel.onerror = opusCheck(channel)) + ); + return music; +};
\ No newline at end of file diff --git a/webAO/viewport/utils/createSfxAudio.ts b/webAO/viewport/utils/createSfxAudio.ts new file mode 100644 index 0000000..7e03563 --- /dev/null +++ b/webAO/viewport/utils/createSfxAudio.ts @@ -0,0 +1,9 @@ +import { AO_HOST } from "../../client/aoHost"; + +export const createSfxAudio = () => { + const sfxAudio = document.getElementById( + "client_sfxaudio" + ) as HTMLAudioElement; + sfxAudio.src = `${AO_HOST}sounds/general/sfx-realization.opus`; + return sfxAudio; +};
\ No newline at end of file diff --git a/webAO/viewport/utils/createShoutAudio.ts b/webAO/viewport/utils/createShoutAudio.ts new file mode 100644 index 0000000..8211116 --- /dev/null +++ b/webAO/viewport/utils/createShoutAudio.ts @@ -0,0 +1,9 @@ +import { AO_HOST } from "../../client/aoHost"; + +export const createShoutAudio = () => { + const shoutAudio = document.getElementById( + "client_shoutaudio" + ) as HTMLAudioElement; + shoutAudio.src = `${AO_HOST}misc/default/objection.opus`; + return shoutAudio; +};
\ No newline at end of file diff --git a/webAO/viewport/utils/createTestimonyAudio.ts b/webAO/viewport/utils/createTestimonyAudio.ts new file mode 100644 index 0000000..2ff98f6 --- /dev/null +++ b/webAO/viewport/utils/createTestimonyAudio.ts @@ -0,0 +1,9 @@ +import { AO_HOST } from '../../client/aoHost' + +export const createTestimonyAudio = () => { + const testimonyAudio = document.getElementById( + "client_testimonyaudio" + ) as HTMLAudioElement; + testimonyAudio.src = `${AO_HOST}sounds/general/sfx-guilty.opus`; + return testimonyAudio; +};
\ No newline at end of file diff --git a/webAO/viewport/utils/handleICSpeaking.ts b/webAO/viewport/utils/handleICSpeaking.ts new file mode 100644 index 0000000..e2d147d --- /dev/null +++ b/webAO/viewport/utils/handleICSpeaking.ts @@ -0,0 +1,311 @@ +import { ChatMsg } from "../interfaces/ChatMsg"; +import { client } from "../../client"; +import { appendICLog } from "../../client/appendICLog"; +import { checkCallword } from "../../client/checkCallword"; +import setEmote from "../../client/setEmote"; +import { AO_HOST } from "../../client/aoHost"; +import { SHOUTS } from "../constants/shouts"; +import getAnimLength from "../../utils/getAnimLength"; +import { setChatbox } from "../../dom/setChatbox"; +import { resizeChatbox } from "../../dom/resizeChatbox"; +import transparentPng from "../../constants/transparentPng"; +import { COLORS } from "../constants/colors"; +import mlConfig from "../../utils/aoml"; + +const attorneyMarkdown = mlConfig(AO_HOST); + +export let startFirstTickCheck: boolean; +export const setStartFirstTickCheck = (val: boolean) => {startFirstTickCheck = val} +export let startSecondTickCheck: boolean; +export const setStartSecondTickCheck = (val: boolean) => {startSecondTickCheck = val} +export let startThirdTickCheck: boolean; +export const setStartThirdTickCheck = (val: boolean) => {startThirdTickCheck = val} +/** + * Sets a new emote. + * This sets up everything before the tick() loops starts + * a lot of things can probably be moved here, like starting the shout animation if there is one + * TODO: the preanim logic, on the other hand, should probably be moved to tick() + * @param {object} chatmsg the new chat message + */ +export const handle_ic_speaking = async (playerChatMsg: ChatMsg) => { + client.viewport.setChatmsg(playerChatMsg); + client.viewport.setTextNow(""); + client.viewport.setSfxPlayed(0); + client.viewport.setTickTimer(0); + client.viewport.setAnimating(true); + + startFirstTickCheck = true; + startSecondTickCheck = false; + startThirdTickCheck = false; + let charLayers = document.getElementById("client_char")!; + let pairLayers = document.getElementById("client_pair_char")!; + // stop updater + clearTimeout(client.viewport.updater); + + // stop last sfx from looping any longer + client.viewport.getSfxAudio().loop = false; + + const fg = <HTMLImageElement>document.getElementById("client_fg"); + const gamewindow = document.getElementById("client_gamewindow")!; + const waitingBox = document.getElementById("client_chatwaiting")!; + + // Reset CSS animation + gamewindow.style.animation = ""; + waitingBox.style.opacity = "0"; + + const eviBox = document.getElementById("client_evi")!; + + if (client.viewport.getLastEvidence() !== client.viewport.getChatmsg().evidence) { + eviBox.style.opacity = "0"; + eviBox.style.height = "0%"; + } + client.viewport.setLastEvidence(client.viewport.getChatmsg().evidence); + + const validSides: string[] = ["def", "pro", "wit"]; // these are for the full view pan, the other positions use 'client_char' + if (validSides.includes(client.viewport.getChatmsg().side)) { + charLayers = document.getElementById(`client_${client.viewport.getChatmsg().side}_char`); + pairLayers = document.getElementById(`client_${client.viewport.getChatmsg().side}_pair_char`); + } + + const chatContainerBox = document.getElementById("client_chatcontainer")!; + const nameBoxInner = document.getElementById("client_inner_name")!; + const chatBoxInner = document.getElementById("client_inner_chat")!; + + const displayname = + (<HTMLInputElement>document.getElementById("showname")).checked && + client.viewport.getChatmsg().showname !== "" + ? client.viewport.getChatmsg().showname! + : client.viewport.getChatmsg().nameplate!; + + // Clear out the last message + chatBoxInner.innerText = client.viewport.getTextNow(); + nameBoxInner.innerText = displayname; + + if (client.viewport.getLastCharacter() !== client.viewport.getChatmsg().name) { + charLayers.style.opacity = "0"; + pairLayers.style.opacity = "0"; + } + + client.viewport.setLastCharacter(client.viewport.getChatmsg().name); + + appendICLog(client.viewport.getChatmsg().content, client.viewport.getChatmsg().showname, client.viewport.getChatmsg().nameplate); + + checkCallword(client.viewport.getChatmsg().content, client.viewport.getSfxAudio()); + + setEmote( + AO_HOST, + client, + client.viewport.getChatmsg().name!.toLowerCase(), + client.viewport.getChatmsg().sprite!, + "(a)", + false, + client.viewport.getChatmsg().side + ); + + if (client.viewport.getChatmsg().other_name) { + setEmote( + AO_HOST, + client, + client.viewport.getChatmsg().other_name.toLowerCase(), + client.viewport.getChatmsg().other_emote!, + "(a)", + false, + client.viewport.getChatmsg().side + ); + } + + // gets which shout shall played + const shoutSprite = <HTMLImageElement>( + document.getElementById("client_shout") + ); + + const shout = SHOUTS[client.viewport.getChatmsg().objection]; + if (shout) { + // Hide message box + chatContainerBox.style.opacity = "0"; + if (client.viewport.getChatmsg().objection === 4) { + shoutSprite.src = `${AO_HOST}characters/${encodeURI( + client.viewport.getChatmsg().name!.toLowerCase() + )}/custom.gif`; + } else { + shoutSprite.src = client.resources[shout].src; + shoutSprite.style.animation = "bubble 700ms steps(10, jump-both)"; + } + shoutSprite.style.opacity = "1"; + + client.viewport.shoutaudio.src = `${AO_HOST}characters/${encodeURI( + client.viewport.getChatmsg().name.toLowerCase() + )}/${shout}.opus`; + client.viewport.shoutaudio.play(); + client.viewport.setShoutTimer(client.resources[shout].duration); + } else { + client.viewport.setShoutTimer(0); + } + + client.viewport.getChatmsg().startpreanim = true; + let gifLength = 0; + + if (client.viewport.getChatmsg().type === 1 && client.viewport.getChatmsg().preanim !== "-") { + //we have a preanim + chatContainerBox.style.opacity = "0"; + + gifLength = await getAnimLength( + `${AO_HOST}characters/${encodeURI( + client.viewport.getChatmsg().name!.toLowerCase() + )}/${encodeURI(client.viewport.getChatmsg().preanim)}` + ); + client.viewport.getChatmsg().startspeaking = false; + } else { + client.viewport.getChatmsg().startspeaking = true; + if (client.viewport.getChatmsg().content.trim() !== "") chatContainerBox.style.opacity = "1"; + } + client.viewport.getChatmsg().preanimdelay = gifLength; + const setAside = { + position: client.viewport.getChatmsg().side, + showSpeedLines: false, + showDesk: false, + }; + let skipoffset: boolean = false; + if (client.viewport.getChatmsg().type === 5) { + setAside.showSpeedLines = true; + setAside.showDesk = false; + client.viewport.set_side(setAside); + } else { + switch (Number(client.viewport.getChatmsg().deskmod)) { + case 0: //desk is hidden + setAside.showSpeedLines = false; + setAside.showDesk = false; + client.viewport.set_side(setAside); + break; + case 1: //desk is shown + setAside.showSpeedLines = false; + setAside.showDesk = true; + client.viewport.set_side(setAside); + break; + case 2: //desk is hidden during preanim, but shown during idle/talk + setAside.showSpeedLines = false; + setAside.showDesk = false; + client.viewport.set_side(setAside); + break; + case 3: //opposite of 2 + setAside.showSpeedLines = false; + setAside.showDesk = false; + client.viewport.set_side(setAside); + break; + case 4: //desk is hidden, character offset is ignored, pair character is hidden during preanim, normal behavior during idle/talk + setAside.showSpeedLines = false; + setAside.showDesk = false; + client.viewport.set_side(setAside); + skipoffset = true; + break; + case 5: //opposite of 4 + setAside.showSpeedLines = false; + setAside.showDesk = true; + client.viewport.set_side(setAside); + break; + default: + setAside.showSpeedLines = false; + setAside.showDesk = true; + client.viewport.set_side(setAside); + break; + } + } + + setChatbox(client.viewport.getChatmsg().chatbox); + resizeChatbox(); + + if (!skipoffset) { + // Flip the character + charLayers.style.transform = + client.viewport.getChatmsg().flip === 1 ? "scaleX(-1)" : "scaleX(1)"; + pairLayers.style.transform = + client.viewport.getChatmsg().other_flip === 1 ? "scaleX(-1)" : "scaleX(1)"; + + // Shift by the horizontal offset + switch (client.viewport.getChatmsg().side) { + case "wit": + pairLayers.style.left = `${200 + Number(client.viewport.getChatmsg().other_offset[0])}%`; + charLayers.style.left = `${200 + Number(client.viewport.getChatmsg().self_offset[0])}%`; + break; + case "pro": + pairLayers.style.left = `${400 + Number(client.viewport.getChatmsg().other_offset[0])}%`; + charLayers.style.left = `${400 + Number(client.viewport.getChatmsg().self_offset[0])}%`; + break; + default: + pairLayers.style.left = `${Number(client.viewport.getChatmsg().other_offset[0])}%`; + charLayers.style.left = `${Number(client.viewport.getChatmsg().self_offset[0])}%`; + break; + } + + // New vertical offsets + pairLayers.style.top = `${Number(client.viewport.getChatmsg().other_offset[1])}%`; + charLayers.style.top = `${Number(client.viewport.getChatmsg().self_offset[1])}%`; + } + + client.viewport.blipChannels.forEach( + (channel: HTMLAudioElement) => + (channel.src = `${AO_HOST}sounds/general/sfx-blip${encodeURI( + client.viewport.getChatmsg().blips.toLowerCase() + )}.opus`) + ); + + // process markup + if (client.viewport.getChatmsg().content.startsWith("~~")) { + chatBoxInner.style.textAlign = "center"; + client.viewport.getChatmsg().content = client.viewport.getChatmsg().content.substring(2, client.viewport.getChatmsg().content.length); + } else { + chatBoxInner.style.textAlign = "inherit"; + } + + // apply effects + fg.style.animation = ""; + const effectName = client.viewport.getChatmsg().effects[0].toLowerCase(); + const badEffects = ["", "-", "none"]; + if (effectName.startsWith("rain")) { + (<HTMLLinkElement>document.getElementById("effect_css")).href = "styles/effects/rain.css"; + let intensity = 200; + if (effectName.endsWith("weak")) { + intensity = 100; + } else if (effectName.endsWith("strong")) { + intensity = 400; + } + if (intensity < fg.childElementCount) + fg.innerHTML = ''; + else + intensity = intensity - fg.childElementCount; + + for (let i = 0; i < intensity; i++) { + let drop = document.createElement("p"); + drop.style.left = (Math.random() * 100) + "%"; + drop.style.animationDelay = String(Math.random()) + "s"; + fg.appendChild(drop) + } + } else if ( + client.viewport.getChatmsg().effects[0] && + !badEffects.includes(effectName) + ) { + (<HTMLLinkElement>document.getElementById("effect_css")).href = ""; + fg.innerHTML = ''; + const baseEffectUrl = `${AO_HOST}themes/default/effects/`; + fg.src = `${baseEffectUrl}${encodeURI(effectName)}.webp`; + } else { + fg.innerHTML = ''; + fg.src = transparentPng; + } + + + charLayers.style.opacity = "1"; + + const soundChecks = ["0", "1", "", undefined]; + if (soundChecks.some((check) => client.viewport.getChatmsg().sound === check)) { + client.viewport.getChatmsg().sound = client.viewport.getChatmsg().effects[2]; + } + + client.viewport.getChatmsg().parsed = await attorneyMarkdown.applyMarkdown( + client.viewport.getChatmsg().content, + + COLORS[client.viewport.getChatmsg().color] + + ); + client.viewport.chat_tick(); +};
\ No newline at end of file diff --git a/webAO/viewport/utils/initTestimonyUpdater.ts b/webAO/viewport/utils/initTestimonyUpdater.ts new file mode 100644 index 0000000..e6f6e9d --- /dev/null +++ b/webAO/viewport/utils/initTestimonyUpdater.ts @@ -0,0 +1,31 @@ +import { Testimony } from '../interfaces/Testimony' +import { client, UPDATE_INTERVAL } from '../../client' +/** + * Intialize testimony updater + */ +export const initTestimonyUpdater = () => { + const testimonyFilenames: Testimony = { + 1: "witnesstestimony", + 2: "crossexamination", + 3: "notguilty", + 4: "guilty", + }; + + const testimony = testimonyFilenames[client.testimonyID]; + if (!testimony) { + console.warn(`Invalid testimony ID ${client.testimonyID}`); + return; + } + + client.viewport.testimonyAudio.src = client.resources[testimony].sfx; + client.viewport.testimonyAudio.play(); + + const testimonyOverlay = <HTMLImageElement>( + document.getElementById("client_testimony") + ); + testimonyOverlay.src = client.resources[testimony].src; + testimonyOverlay.style.opacity = "1"; + + client.viewport.setTestimonyTimer(0); + client.viewport.setTestimonyUpdater(setTimeout(() => client.viewport.updateTestimony(), UPDATE_INTERVAL)); +};
\ No newline at end of file diff --git a/webAO/viewport/utils/setSide.ts b/webAO/viewport/utils/setSide.ts new file mode 100644 index 0000000..3726e83 --- /dev/null +++ b/webAO/viewport/utils/setSide.ts @@ -0,0 +1,90 @@ +import { positions } from '../constants/positions' +import { AO_HOST } from '../../client/aoHost' +import { client } from '../../client' +import tryUrls from '../../utils/tryUrls'; +import fileExists from '../../utils/fileExists'; + +/** + * Changes the viewport background based on a given position. + * + * Valid positions: `def, pro, hld, hlp, wit, jud, jur, sea` + * @param {string} position the position to change into + */ +export const set_side = async ({ + position, + showSpeedLines, + showDesk, +}: { + position: string; + showSpeedLines: boolean; + showDesk: boolean; +}) => { + const view = document.getElementById("client_fullview")!; + let bench: HTMLImageElement; + if (['def','pro','wit'].includes(position)) { + bench = <HTMLImageElement>( + document.getElementById(`client_${position}_bench`) + ); + } else { + bench = <HTMLImageElement>document.getElementById("client_bench_classic"); + } + + let court: HTMLImageElement; + if ("def,pro,wit".includes(position)) { + court = <HTMLImageElement>( + document.getElementById(`client_court_${position}`) + ); + } else { + court = <HTMLImageElement>document.getElementById("client_court_classic"); + } + + let bg; + let desk; + let speedLines; + + if ("def,pro,hld,hlp,wit,jud,jur,sea".includes(position)) { + bg = positions[position].bg; + desk = positions[position].desk; + speedLines = positions[position].speedLines; + } else { + bg = `${position}`; + desk = { ao2: `${position}_overlay.png`, ao1: "_overlay.png" }; + speedLines = "defense_speedlines.gif"; + } + + if (showSpeedLines === true) { + court.src = `${AO_HOST}themes/default/${encodeURI(speedLines)}`; + } else { + court.src = await tryUrls(client.viewport.getBackgroundFolder() + bg); + } + + + if (showDesk === true && desk) { + const deskFilename = (await fileExists(client.viewport.getBackgroundFolder() + desk.ao2)) + ? desk.ao2 + : desk.ao1; + bench.src = client.viewport.getBackgroundFolder() + deskFilename; + bench.style.opacity = "1"; + } else { + bench.style.opacity = "0"; + } + + if ("def,pro,wit".includes(position)) { + view.style.display = ""; + document.getElementById("client_classicview")!.style.display = "none"; + switch (position) { + case "def": + view.style.left = "0"; + break; + case "wit": + view.style.left = "-200%"; + break; + case "pro": + view.style.left = "-400%"; + break; + } + } else { + view.style.display = "none"; + document.getElementById("client_classicview").style.display = ""; + } +}; diff --git a/webAO/viewport/viewport.ts b/webAO/viewport/viewport.ts new file mode 100644 index 0000000..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; |
