diff --git a/README.md b/README.md index dd790f7..db39568 100644 --- a/README.md +++ b/README.md @@ -29,20 +29,23 @@ If you would like to contribute to Jankboard 2, there's only a few simple steps - If you don't have access to a development environment that supports running standalone executables (eg. Github Codespaces), you can try running `npm run dev` instead of `npm run tauri dev`, which will open a development server at `localhost:5173` with the frontend running in the web. However, this may break at any time as critical functionality is more directly attached to the Rust backend. - If for some reason you need to install and use the Python backend while we are migrating to Rust, run `poetry install --no-root` in the root directory of the project to install dependencies. You can start the server with `poetry run flask --app app/server.py run --host localhost --port 1280` (it must be running at port `1280` for the frontend to detect it). -## Current progress +## Current progress and improvements over (original) Jankboard -- Basic UI layout complete -- Media player working with a few small issues -- App system working smoothly -- Camera feed likely working -- Frontend syncs basic telemetry data with robot through the same Socket-IO code that powered Jankboard v1 -- Notification service installed, with Toast and audio capability +- Layout, toasts/notifications, music player, and app system ported. +- Toast and audio cue system is much more robust +- Transitions added almost everywhere to make things smoother +- Tauri app created successfully, currently still using Flask backend +- Visualization vastly improved with Threlte (Three.js) powered 3D robot simulation +- Robot model ported successfully via massive optimization through polygon decimation +- Added settings app with options to disable certain features and developer tools for testing ## TODO - Camera cutout overlay - Overhaul audio player system -- Robot visualization (3D, in Threlte). -- Overhaul backend +- Overhaul visualization (especially camera) +- Overhaul backend in Rust - Further integrate telemetry (like GPWS, collision warning, etc) - Finish re-creating / adding various voice alerts and sequences +- Create dynamic voice prompt system to support new languages very easily +- Add dynamic voice prompt fallback to support incremental voice prompt migration diff --git a/client/package-lock.json b/client/package-lock.json index c3afd7d..218f778 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -11,12 +11,14 @@ "@fontsource/roboto": "^5.0.8", "@threlte/core": "^7.1.0", "@threlte/extras": "^8.8.0", + "camera-controls": "^2.8.3", "howler": "^2.2.4", "material-icons": "^1.13.12", "material-symbols": "^0.15.0", "overlayscrollbars-svelte": "^0.5.3", "socket.io-client": "^4.7.4", "svelte-french-toast": "^1.2.0", + "svelte-tweakpane-ui": "^1.2.1", "three": "^0.161.0" }, "devDependencies": { @@ -1035,6 +1037,11 @@ "integrity": "sha512-BRbo1fOtyVbhfLyuCWw6wAWp+U8UQle+ZXu84MYYWzYSEB28dyfnRBIE99eoG+qdAC0po6L2ScIEivcT07UaMA==", "dev": true }, + "node_modules/@tweakpane/core": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tweakpane/core/-/core-2.0.3.tgz", + "integrity": "sha512-qHci4XA1Wngpwy8IzsLh5JEdscz8aDti/9YhyOaq01si+cgNDaZfwzTtXdn1+xTxSnCM+pW4Zb2/4eqn+K1ATw==" + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -1300,6 +1307,14 @@ "node": ">= 6" } }, + "node_modules/camera-controls": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.8.3.tgz", + "integrity": "sha512-zFjqUR6onLkG+z1A6vAWfzovxZxWVSvp6e5t3lfZgfgPZtX3n74aykNAUaoRbq8Y3tOxadHkDjbfGDOP9hFf2w==", + "peerDependencies": { + "three": ">=0.126.1" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001588", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz", @@ -1592,9 +1607,7 @@ "node_modules/esm-env": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", - "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==", - "dev": true, - "peer": true + "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==" }, "node_modules/estree-walker": { "version": "3.0.3", @@ -1604,6 +1617,19 @@ "@types/estree": "^1.0.0" } }, + "node_modules/fast-copy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.1.tgz", + "integrity": "sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==" + }, + "node_modules/fast-equals": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", + "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -2972,6 +2998,17 @@ "svelte": "^3.19.0 || ^4.0.0" } }, + "node_modules/svelte-local-storage-store": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/svelte-local-storage-store/-/svelte-local-storage-store-0.6.4.tgz", + "integrity": "sha512-45WoY2vSGPQM1sIQJ9jTkPPj20hYeqm+af6mUGRFSPP5WglZf36YYoZqwmZZ8Dt/2SU8lem+BTA8/Z/8TkqNLg==", + "engines": { + "node": ">=0.14" + }, + "peerDependencies": { + "svelte": "^3.48.0 || >4.0.0" + } + }, "node_modules/svelte-preprocess": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.3.tgz", @@ -3035,6 +3072,115 @@ } } }, + "node_modules/svelte-tweakpane-ui": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/svelte-tweakpane-ui/-/svelte-tweakpane-ui-1.2.1.tgz", + "integrity": "sha512-F62AFvZiqhXa0E3HMpUdmdWWyus/C+nTBFrFd/vEhHP6dYOQ4EN9qwvjdybL90DZFTIwkZe3w4oSbEGn1muKkQ==", + "dependencies": { + "@0b5vr/tweakpane-plugin-profiler": "^0.4.1", + "@0b5vr/tweakpane-plugin-rotation": "^0.2.0", + "@kitschpatrol/tweakpane-image-plugin": "^2.0.0", + "@pangenerator/tweakpane-textarea-plugin": "^2.0.0", + "@tweakpane/core": "^2.0.3", + "@tweakpane/plugin-camerakit": "^0.3.0", + "@tweakpane/plugin-essentials": "^0.2.1", + "esm-env": "^1.0.0", + "fast-copy": "^3.0.1", + "fast-equals": "^5.0.1", + "nanoid": "^5.0.6", + "svelte-local-storage-store": "^0.6.4", + "tweakpane": "^4.0.3", + "tweakpane-plugin-waveform": "^1.0.0" + }, + "engines": { + "node": ">=18.0.0", + "pnpm": ">=8.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0" + } + }, + "node_modules/svelte-tweakpane-ui/node_modules/@0b5vr/tweakpane-plugin-profiler": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@0b5vr/tweakpane-plugin-profiler/-/tweakpane-plugin-profiler-0.4.1.tgz", + "integrity": "sha512-jgkPbT24eQ7isj8F7/IsbdqrwvBoWBmwjqxdP35smD2D6xsx+9viR57SKBxi9PxTZDEayicmCzBk++0PTqRnBg==", + "peerDependencies": { + "tweakpane": "^4.0.0" + } + }, + "node_modules/svelte-tweakpane-ui/node_modules/@0b5vr/tweakpane-plugin-rotation": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@0b5vr/tweakpane-plugin-rotation/-/tweakpane-plugin-rotation-0.2.0.tgz", + "integrity": "sha512-LK+84kNTusEepVwiKH6ib/Pd+5RxI3UC4rHxn5c14GO58QS49Hh0ft3hFXt/NDzYEST17Q9qg96BcpclhCzYYQ==", + "peerDependencies": { + "tweakpane": "^4.0.0" + } + }, + "node_modules/svelte-tweakpane-ui/node_modules/@kitschpatrol/tweakpane-image-plugin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@kitschpatrol/tweakpane-image-plugin/-/tweakpane-image-plugin-2.0.0.tgz", + "integrity": "sha512-BzEZqIhD/dM7AW0Ebv+309L4k8ZZJ5fC9Zks4sozVK3FwJooviE6JzaFAuB7k0M5oX45Wyn59tQXdHafgsP3YA==", + "peerDependencies": { + "tweakpane": "^4.0.0" + } + }, + "node_modules/svelte-tweakpane-ui/node_modules/@pangenerator/tweakpane-textarea-plugin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@pangenerator/tweakpane-textarea-plugin/-/tweakpane-textarea-plugin-2.0.0.tgz", + "integrity": "sha512-BERPuuyJYWvtJzXh4wtgYspza0ihigE2m4qs57ERKtWG59+lI2t/2TOXlwz7Xyx/QEIH25uO1g732YCljgKaUw==", + "peerDependencies": { + "tweakpane": "^4.0.0" + } + }, + "node_modules/svelte-tweakpane-ui/node_modules/@tweakpane/plugin-camerakit": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tweakpane/plugin-camerakit/-/plugin-camerakit-0.3.0.tgz", + "integrity": "sha512-6UwgwDKU+oaAgXJ2D/pOoIpEAZts0RyeLmVzBJGs+VVNqSfkiHzL0i5XD+XnmSL2PaLXBne0dlz0bYOrjmeELw==", + "peerDependencies": { + "tweakpane": "^4.0.0-beta.2" + } + }, + "node_modules/svelte-tweakpane-ui/node_modules/@tweakpane/plugin-essentials": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@tweakpane/plugin-essentials/-/plugin-essentials-0.2.1.tgz", + "integrity": "sha512-VbFU1/uD+CJNFQdfLXUOLjeG5HyUZH97Ox9CxmyVetg1hqjVun3C83HAGFULyhKzl8tSgii8jr304r8QpdHwzQ==", + "peerDependencies": { + "tweakpane": "^4.0.0" + } + }, + "node_modules/svelte-tweakpane-ui/node_modules/nanoid": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.6.tgz", + "integrity": "sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/svelte-tweakpane-ui/node_modules/tweakpane": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tweakpane/-/tweakpane-4.0.3.tgz", + "integrity": "sha512-BlcWOAe8oe4c+k9pmLBARGdWB6MVZMszayekkixQXTgkxTaYoTUpHpwVEp+3HkoamZkomodpbBf0CkguIHTgLg==", + "funding": { + "url": "https://github.com/sponsors/cocopon" + } + }, + "node_modules/svelte-tweakpane-ui/node_modules/tweakpane-plugin-waveform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tweakpane-plugin-waveform/-/tweakpane-plugin-waveform-1.0.0.tgz", + "integrity": "sha512-fyTRe6Emt7YpgHC5iiTZgk6RHflNm5VIOAsl2+l3mm96+KE8I+7sNPeyADxKcfcQF23c7/R3La5WNhaHNyeJag==", + "peerDependencies": { + "tweakpane": "^4.0.0" + } + }, "node_modules/svelte-writable-derived": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/svelte-writable-derived/-/svelte-writable-derived-3.1.0.tgz", diff --git a/client/package.json b/client/package.json index 634fbe2..6c81629 100644 --- a/client/package.json +++ b/client/package.json @@ -31,12 +31,14 @@ "@fontsource/roboto": "^5.0.8", "@threlte/core": "^7.1.0", "@threlte/extras": "^8.8.0", + "camera-controls": "^2.8.3", "howler": "^2.2.4", "material-icons": "^1.13.12", "material-symbols": "^0.15.0", "overlayscrollbars-svelte": "^0.5.3", "socket.io-client": "^4.7.4", "svelte-french-toast": "^1.2.0", + "svelte-tweakpane-ui": "^1.2.1", "three": "^0.161.0" } } diff --git a/client/src/lib/Dashboard/Visualization/CameraControls/CameraControls.svelte b/client/src/lib/Dashboard/Visualization/CameraControls/CameraControls.svelte new file mode 100644 index 0000000..f6d3935 --- /dev/null +++ b/client/src/lib/Dashboard/Visualization/CameraControls/CameraControls.svelte @@ -0,0 +1,106 @@ + + + + + { + disableAutoRotate = true + }} + on:zoom={e => { + console.log('zoomstart', e) + }} + on:controlend={() => { + disableAutoRotate = false + }} + {...$$restProps} + bind:this={$forwardingComponent} +> + + diff --git a/client/src/lib/Dashboard/Visualization/CameraControls/CameraControls.svelte.d.ts b/client/src/lib/Dashboard/Visualization/CameraControls/CameraControls.svelte.d.ts new file mode 100644 index 0000000..6bc8f0e --- /dev/null +++ b/client/src/lib/Dashboard/Visualization/CameraControls/CameraControls.svelte.d.ts @@ -0,0 +1,16 @@ +import type { Events, Props, Slots } from '@threlte/core' +import CC from 'camera-controls' +import type { SvelteComponent } from 'svelte' + +export type CameraControlsProps = Props & { + autoRotate?: boolean + autoRotateSpeed?: number +} +export type CameraControlsEvents = Events +export type CameraControlsSlots = Slots + +export default class CameraControls extends SvelteComponent< + CameraControlsProps, + CameraControlsEvents, + CameraControlsSlots +> {} diff --git a/client/src/lib/Dashboard/Visualization/CameraControls/Scene.svelte b/client/src/lib/Dashboard/Visualization/CameraControls/Scene.svelte new file mode 100644 index 0000000..c39e00d --- /dev/null +++ b/client/src/lib/Dashboard/Visualization/CameraControls/Scene.svelte @@ -0,0 +1,122 @@ + + + { + ref.lookAt(0, 1, 0); + }} +> + { + $cameraControls = ref; + }} + /> + + + + + { + // @ts-expect-error + $mesh = ref; + }} +/> + + diff --git a/client/src/lib/Dashboard/Visualization/CameraControls/utils/cameraStore.ts b/client/src/lib/Dashboard/Visualization/CameraControls/utils/cameraStore.ts new file mode 100644 index 0000000..37a02c6 --- /dev/null +++ b/client/src/lib/Dashboard/Visualization/CameraControls/utils/cameraStore.ts @@ -0,0 +1,7 @@ +import type CameraControls from "camera-controls"; +import { writable } from "svelte/store"; +import type { Mesh, Object3DEventMap } from "three"; +import type { Group } from "three/examples/jsm/libs/tween.module.js"; + +export const cameraControls = writable(); +export const mesh = writable(); diff --git a/client/src/lib/Dashboard/Visualization/CameraControls/utils/useControlsContext.ts b/client/src/lib/Dashboard/Visualization/CameraControls/utils/useControlsContext.ts new file mode 100644 index 0000000..c8862b9 --- /dev/null +++ b/client/src/lib/Dashboard/Visualization/CameraControls/utils/useControlsContext.ts @@ -0,0 +1,20 @@ +import { useThrelteUserContext } from '@threlte/core' +import { writable, type Writable } from 'svelte/store' +import type { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' + +type ControlsContext = { + orbitControls: Writable +} + +/** + * ### `useControlsContext` + * + * This hook is used to register the `OrbitControls` instance with the + * `ControlsContext`. We're using this context to enable and disable the + * controls when the user is interacting with the TransformControls. + */ +export const useControlsContext = (): ControlsContext => { + return useThrelteUserContext('threlte-controls', { + orbitControls: writable(undefined), + }) +} diff --git a/client/src/lib/Dashboard/Visualization/Scene.svelte b/client/src/lib/Dashboard/Visualization/Scene.svelte index b60c18e..3347b0c 100644 --- a/client/src/lib/Dashboard/Visualization/Scene.svelte +++ b/client/src/lib/Dashboard/Visualization/Scene.svelte @@ -1,23 +1,23 @@ @@ -145,11 +145,12 @@ - +/> --> diff --git a/client/src/lib/Dashboard/Visualization/Visualization.svelte b/client/src/lib/Dashboard/Visualization/Visualization.svelte index 4ae23fe..bdde5a9 100644 --- a/client/src/lib/Dashboard/Visualization/Visualization.svelte +++ b/client/src/lib/Dashboard/Visualization/Visualization.svelte @@ -1,6 +1,39 @@ diff --git a/client/src/lib/Notifications/notifications.ts b/client/src/lib/Notifications/notifications.ts index 3cf2338..7262e74 100644 --- a/client/src/lib/Notifications/notifications.ts +++ b/client/src/lib/Notifications/notifications.ts @@ -1,113 +1,147 @@ -import { toast } from 'svelte-french-toast' -import type { ToastOptions } from 'svelte-french-toast' -import InfoIcon from './InfoIcon.svelte' -import { Howl } from 'howler' -import WarnIcon from './WarnIcon.svelte' +import { toast } from "svelte-french-toast"; +import type { ToastOptions } from "svelte-french-toast"; +import InfoIcon from "./InfoIcon.svelte"; +import { Howl } from "howler"; +import WarnIcon from "./WarnIcon.svelte"; interface NotificationOptions extends ToastOptions { - withAudio?: boolean - src?: string - onComplete?: () => void + withAudio?: boolean; + src?: string; + onComplete?: () => void; } // get colors from https://tailwindcss.com/docs/customizing-colors export class Notifications { - private static readonly defaultDuration = 3000 + private static readonly defaultDuration = 3000; public static success(message: string, options?: NotificationOptions) { if (options?.withAudio && !options.src) - throw new Error('No audio source provided') + throw new Error("No audio source provided"); const onComplete = () => { - if (options?.onComplete) options.onComplete() - } + if (options?.onComplete) options.onComplete(); + }; const sendToast = (duration: number) => { toast.success(message, { style: - 'padding: 25px; font-size: 1.5rem; background-color: #15803d; color: #fafafa; gap: 0.5rem; user-select: none; max-width: 70vw;', + "padding: 25px; font-size: 1.5rem; background-color: #15803d; color: #fafafa; gap: 0.5rem; user-select: none; max-width: 70vw;", duration, ...options, - }) - } + }); + }; if (options?.withAudio && options?.src) { - let sound: Howl + let sound: Howl; sound = new Howl({ src: [options.src], preload: true, autoplay: true, onload: () => { - let duration = sound.duration() * 1000 - sendToast(duration) - setTimeout(onComplete, duration) + let duration = sound.duration() * 1000; + sendToast(duration); + setTimeout(onComplete, duration); }, - }) + }); } else { - sendToast(this.defaultDuration) - setTimeout(onComplete, this.defaultDuration) + sendToast(this.defaultDuration); + setTimeout(onComplete, this.defaultDuration); + } + } + public static error(message: string, options?: NotificationOptions) { + if (options?.withAudio && !options.src) + throw new Error("No audio source provided"); + + const onComplete = () => { + if (options?.onComplete) options.onComplete(); + }; + + const sendToast = (duration: number) => { + toast.error(message, { + style: + "padding: 25px; font-size: 1.5rem; background-color: #dc2626; color: #fafafa; gap: 0.5rem; user-select: none; max-width: 70vw;", + duration, + ...options, + }); + }; + + if (options?.withAudio && options?.src) { + let sound: Howl; + sound = new Howl({ + src: [options.src], + preload: true, + autoplay: true, + onload: () => { + let duration = sound.duration() * 1000; + sendToast(duration); + setTimeout(onComplete, duration); + }, + }); + } else { + sendToast(this.defaultDuration); + setTimeout(onComplete, this.defaultDuration); } } public static info(message: string, options?: NotificationOptions) { const onComplete = () => { - if (options?.onComplete) options.onComplete() - } + if (options?.onComplete) options.onComplete(); + }; const sendToast = (duration: number) => { toast(message, { style: - 'padding: 25px; font-size: 1.5rem; gap: 0.5rem; user-select: none; max-width-600px; max-width: 70vw;', + "padding: 25px; font-size: 1.5rem; gap: 0.5rem; user-select: none; max-width-600px; max-width: 70vw;", icon: InfoIcon, duration, ...options, - }) - } + }); + }; if (options?.withAudio && options?.src) { - let sound: Howl + let sound: Howl; sound = new Howl({ src: [options.src], preload: true, autoplay: true, onload: () => { - let duration = sound.duration() * 1000 - sendToast(duration) - setTimeout(onComplete, duration) + let duration = sound.duration() * 1000; + sendToast(duration); + setTimeout(onComplete, duration); }, - }) + }); } else { - sendToast(this.defaultDuration) - setTimeout(onComplete, this.defaultDuration) + sendToast(this.defaultDuration); + setTimeout(onComplete, this.defaultDuration); } } public static warn(message: string, options?: NotificationOptions) { const onComplete = () => { - if (options?.onComplete) options.onComplete() - } + if (options?.onComplete) options.onComplete(); + }; const sendToast = (duration: number) => { toast(message, { style: - 'padding: 25px; font-size: 1.5rem; background-color: #f59e0b; color: #fafafa; gap: 0.5rem; user-select: none; max-width: 70vw;', + "padding: 25px; font-size: 1.5rem; background-color: #f59e0b; color: #fafafa; gap: 0.5rem; user-select: none; max-width: 70vw;", icon: WarnIcon, duration, ...options, - }) - } + }); + }; if (options?.withAudio && options?.src) { - let sound: Howl + let sound: Howl; sound = new Howl({ src: [options.src], preload: true, autoplay: true, onload: () => { - let duration = sound.duration() * 1000 - sendToast(duration) - setTimeout(onComplete, duration) + let duration = sound.duration() * 1000; + sendToast(duration); + setTimeout(onComplete, duration); }, - }) + }); } else { - sendToast(this.defaultDuration) - setTimeout(onComplete, this.defaultDuration) + sendToast(this.defaultDuration); + setTimeout(onComplete, this.defaultDuration); } } public static playAudio(src: string, onComplete: () => void = () => {}) { @@ -116,8 +150,8 @@ export class Notifications { preload: true, autoplay: true, onload: () => { - setTimeout(onComplete, 1000 * sound.duration()) + setTimeout(onComplete, 1000 * sound.duration()); }, - }) + }); } } diff --git a/client/src/lib/Sequences/sequences.ts b/client/src/lib/Sequences/sequences.ts index ee6a911..c5cba6e 100644 --- a/client/src/lib/Sequences/sequences.ts +++ b/client/src/lib/Sequences/sequences.ts @@ -14,47 +14,51 @@ Sequences should be either event-driven or periodic. In the case of periodic sequences, invoke them in the periodicSequence function */ -import { Notifications } from '../Notifications/notifications' -import { sequenceStore } from '../stores/sequenceStore' -import { settingsStore } from '../stores/settingsStore' -import { get } from 'svelte/store' -import getVoicePath from '../utils/getVoicePath' -import { tick } from 'svelte' +import { Notifications } from "../Notifications/notifications"; +import { sequenceStore } from "../stores/sequenceStore"; +import { settingsStore } from "../stores/settingsStore"; +import { get } from "svelte/store"; +import getVoicePath from "../utils/getVoicePath"; +import { tick } from "svelte"; // await a "tick" (a svelte update frame) at the start of every sequence so that // state is synced and no weird side effects occur export const initializationSequence = async () => { - await tick() - Notifications.info('Jankboard initialized!', { + await tick(); + Notifications.info("Jankboard initialized!", { withAudio: true, - src: getVoicePath('jankboard-initialized', 'en'), - }) - setTimeout(() => { - if (get(settingsStore).goWoke) return - Notifications.success('LittenOS is online', { - withAudio: true, - src: getVoicePath('littenos-is-online', 'en'), - }) - setTimeout(() => { - Notifications.warn('Breaching Monte Vista codebase', { + src: getVoicePath("jankboard-initialized"), + onComplete: () => { + if (get(settingsStore).goWoke) { + sequenceStore.update("initializationComplete", true); + periodicSequence(); + return; + } + Notifications.success("LittenOS is online", { withAudio: true, - src: getVoicePath('breaching-monte-vista', 'en'), - }) - setTimeout(() => { - Notifications.playAudio( - getVoicePath('hello-virtual-assistant', 'en'), - () => { - sequenceStore.update('initializationComplete', true) - periodicSequence() - } - ) - }, 3000) - }, 3000) - }, 3000) -} + src: getVoicePath("littenos-is-online"), + onComplete: () => { + Notifications.warn("Breaching Monte Vista codebase", { + withAudio: true, + src: getVoicePath("breaching-monte-vista"), + onComplete: () => { + Notifications.playAudio( + getVoicePath("hello-virtual-assistant"), + () => { + sequenceStore.update("initializationComplete", true); + periodicSequence(); + } + ); + }, + }); + }, + }); + }, + }); +}; -let counter = 1 +let counter = 1; /** * Special sequence that plays invokes itself periodically, started automatically * at the end of the initializationSequence @@ -64,7 +68,7 @@ let counter = 1 * @return void */ const periodicSequence = async () => { - await tick() + await tick(); /** * Returns either true or false based on the provided probability @@ -74,11 +78,11 @@ const periodicSequence = async () => { */ const chance = (probability: number) => { if (probability < 0 || probability > 1) { - throw new Error('Probability must be between 0 and 1') + throw new Error("Probability must be between 0 and 1"); } - return Math.random() < probability * get(settingsStore).randomWeight - } + return Math.random() < probability * get(settingsStore).randomWeight; + }; /** * Calls a callback function at regular intervals. @@ -87,140 +91,141 @@ const periodicSequence = async () => { * @param callback - the function to call */ const every = (seconds: number, callback: () => void) => { - if (counter % seconds === 0) callback() - } + if (counter % seconds === 0) callback(); + }; // add your periodic sequences here every(15, () => { - if (chance(0.2)) breaching1323Sequence() - else if (chance(0.2)) breaching254Sequence() - }) + if (chance(0.2)) breaching1323Sequence(); + else if (chance(0.2)) breaching254Sequence(); + }); every(25, () => { - if (chance(0.05)) bullyingRohanSequence() - else if (chance(0.1)) bypassCoprocessorRestrictionsSequence() - }) + if (chance(0.05)) bullyingRohanSequence(); + else if (chance(0.1)) bypassCoprocessorRestrictionsSequence(); + }); // Dont touch - counter++ - setTimeout(periodicSequence, 1000) -} + counter++; + setTimeout(periodicSequence, 1000); +}; export const criticalFailureIminentSequence = async () => { - await tick() - Notifications.error('Critical robot failure imminent', { + await tick(); + Notifications.error("Critical robot failure imminent", { withAudio: true, - src: getVoicePath('critical-robot-failure', 'en'), - }) -} + src: getVoicePath("critical-robot-failure"), + }); +}; export const collisionDetectedSequence = async () => { - await tick() - Notifications.error('Collision detected', { + await tick(); + Notifications.error("Collision detected", { withAudio: true, - src: getVoicePath('collision-detected', 'en'), - }) -} + src: getVoicePath("collision-detected"), + }); +}; export const collisionImminentSequence = async () => { - await tick() - Notifications.error('Collision imminent', { + await tick(); + Notifications.error("Collision imminent", { withAudio: true, - src: getVoicePath('collision-imminent', 'en'), - }) -} + src: getVoicePath("collision-imminent"), + }); +}; export const cruiseControlEngagedSequence = async () => { - if (get(settingsStore).disableAnnoyances) return - await tick() - Notifications.success('Cruise control engaged', { + if (get(settingsStore).disableAnnoyances) return; + await tick(); + Notifications.success("Cruise control engaged", { withAudio: true, - src: getVoicePath('cruise-control-engaged', 'en'), - }) -} + src: getVoicePath("cruise-control-engaged"), + }); +}; export const retardSequence = async () => { - if (get(settingsStore).goWoke) return - await tick() - Notifications.warn('Retard', { + if (get(settingsStore).goWoke) return; + await tick(); + Notifications.warn("Retard", { withAudio: true, - src: getVoicePath('retard', 'en'), - }) -} + src: getVoicePath("retard"), + }); +}; const breaching254Sequence = async () => { - if (get(settingsStore).disableAnnoyances) return - await tick() - Notifications.warn('Breaching 254 mainframe', { + if (get(settingsStore).disableAnnoyances) return; + await tick(); + Notifications.warn("Breaching 254 mainframe", { withAudio: true, - src: getVoicePath('breaching-254-mainframe', 'en'), - }) -} + src: getVoicePath("breaching-254-mainframe"), + }); +}; const breaching1323Sequence = async () => { - if (get(settingsStore).disableAnnoyances) return - await tick() - Notifications.warn('Breaching 1323 mainframe', { + if (get(settingsStore).disableAnnoyances) return; + await tick(); + Notifications.warn("Breaching 1323 mainframe", { withAudio: true, - src: getVoicePath('breaching-1323-mainframe', 'en'), - }) -} + src: getVoicePath("breaching-1323-mainframe"), + }); +}; const bullyingRohanSequence = async () => { - if (get(settingsStore).disableAnnoyances) return - await tick() - Notifications.info('Bullying Rohan', { + if (get(settingsStore).disableAnnoyances) return; + await tick(); + Notifications.info("Bullying Rohan", { withAudio: true, - src: getVoicePath('bullying-rohan', 'en'), - }) -} + src: getVoicePath("bullying-rohan"), + }); +}; export const userErrorDetectedSequence = async () => { - await tick() - Notifications.error('User error detected', { + await tick(); + Notifications.error("User error detected", { withAudio: true, - src: getVoicePath('user-error-detected', 'en'), - }) -} + src: getVoicePath("user-error-detected"), + }); +}; // hacky way to prevent duplicate infotainment bootups -let infotainmentStarted = false +let infotainmentStarted = false; export const infotainmentBootupSequence = async () => { if ( get(sequenceStore).infotainmentStartedFirstTime || get(settingsStore).disableAnnoyances || infotainmentStarted - ) - return + ) { + return; + } - infotainmentStarted = true - await tick() + infotainmentStarted = true; + await tick(); const sequence = () => { - Notifications.info('Infotainment system buffering', { + Notifications.info("Infotainment system buffering", { withAudio: true, - src: getVoicePath('infotainment-system-buffering', 'en'), - }) - setTimeout(() => { - Notifications.success('Infotainment system online', { - withAudio: true, - src: getVoicePath('infotainment-system-online', 'en'), - onComplete: () => { - sequenceStore.update('infotainmentStartedFirstTime', true) - }, - }) - }, 3000) - } + src: getVoicePath("infotainment-system-buffering"), + onComplete: () => { + Notifications.success("Infotainment system online", { + withAudio: true, + src: getVoicePath("infotainment-system-online"), + onComplete: () => { + sequenceStore.update("infotainmentStartedFirstTime", true); + }, + }); + }, + }); + }; if (!get(sequenceStore).initializationComplete) { - const unsubscribe = sequenceStore.subscribe(data => { + const unsubscribe = sequenceStore.subscribe((data) => { if (data.initializationComplete) { - sequence() - unsubscribe() + sequence(); + unsubscribe(); } - }) + }); } else { - sequence() + sequence(); } -} +}; /** * Waits for the infotainment system to boot up before executing the given sequence. @@ -231,77 +236,77 @@ export const infotainmentBootupSequence = async () => { */ const waitForInfotainmentBootup = (sequence: () => void) => { if (!get(sequenceStore).infotainmentStartedFirstTime) { - const unsubscribe = sequenceStore.subscribe(data => { + const unsubscribe = sequenceStore.subscribe((data) => { if (data.infotainmentStartedFirstTime) { - sequence() - unsubscribe() + sequence(); + unsubscribe(); } - }) + }); } else { - sequence() + sequence(); } -} +}; export const musicPlayerBootupSequence = async () => { if ( get(sequenceStore).musicStartedFirstTime || get(settingsStore).disableAnnoyances ) - return + return; - await tick() + await tick(); - sequenceStore.update('musicStartedFirstTime', true) + sequenceStore.update("musicStartedFirstTime", true); waitForInfotainmentBootup(() => { - Notifications.info('Downloading copyrighted music...', { + Notifications.info("Downloading copyrighted music...", { withAudio: true, - src: getVoicePath('downloading-copyrighted-music', 'en'), - }) - }) -} + src: getVoicePath("downloading-copyrighted-music"), + }); + }); +}; export const gbaEmulatorBootupSequence = async () => { if ( get(sequenceStore).gbaEmulatorStartedFirstTime || get(settingsStore).disableAnnoyances ) - return + return; - await tick() - sequenceStore.update('gbaEmulatorStartedFirstTime', true) + await tick(); + sequenceStore.update("gbaEmulatorStartedFirstTime", true); waitForInfotainmentBootup(() => { - Notifications.info('Loading pirated Nintendo ROMs', { + Notifications.info("Loading pirated Nintendo ROMs", { withAudio: true, - src: getVoicePath('loading-pirated-nintendo', 'en'), - }) - }) -} + src: getVoicePath("loading-pirated-nintendo"), + }); + }); +}; export const doomBootupSequence = async () => { if ( get(sequenceStore).doomStartedFirstTime || get(settingsStore).disableAnnoyances ) - return + return; - await tick() - sequenceStore.update('doomStartedFirstTime', true) + await tick(); + sequenceStore.update("doomStartedFirstTime", true); waitForInfotainmentBootup(() => { - Notifications.success('Doom Engaged', { + Notifications.success("Doom Engaged", { withAudio: true, - src: getVoicePath('doom-engaged', 'en'), - }) - }) -} + src: getVoicePath("doom-engaged"), + }); + }); +}; const bypassCoprocessorRestrictionsSequence = async () => { - if (get(settingsStore).disableAnnoyances) return - await tick() - Notifications.warn('Bypassing coprocessor restrictions', { + if (get(settingsStore).disableAnnoyances) return; + await tick(); + Notifications.warn("Bypassing coprocessor restrictions", { withAudio: true, - src: getVoicePath('bypassing-coprocessor-restrictions', 'en'), - }) -} + src: getVoicePath("bypassing-coprocessor-restrictions"), + }); +}; diff --git a/client/src/lib/stores/settingsStore.ts b/client/src/lib/stores/settingsStore.ts index 1fd43fd..6b816a5 100644 --- a/client/src/lib/stores/settingsStore.ts +++ b/client/src/lib/stores/settingsStore.ts @@ -1,12 +1,15 @@ /* stores global app wide settings */ -import { writable } from 'svelte/store' +import { writable } from "svelte/store"; + +type SupportedLanguage = "en" | "rus"; export interface SettingsStoreData { - disableAnnoyances: boolean - goWoke: boolean - fastStartup: boolean - randomWeight: number + disableAnnoyances: boolean; + goWoke: boolean; + fastStartup: boolean; + randomWeight: number; + voiceLang: SupportedLanguage; } export const defaults: SettingsStoreData = { @@ -14,25 +17,26 @@ export const defaults: SettingsStoreData = { goWoke: false, // go woke (for showing parents or other officials where DEI has taken over), disables "offensive" sequences fastStartup: false, // skip the loading splash screen (for development purposes. Setting this from within the app has no effect.) randomWeight: 1, // the weight of random events (multiplied by the original probability) -} + voiceLang: "en", +}; const createSequenceStore = () => { - const { subscribe, set, update } = writable(defaults) + const { subscribe, set, update } = writable(defaults); return { subscribe, update: ( data: keyof SettingsStoreData, newValue: SettingsStoreData[typeof data] ) => { - update(store => { + update((store) => { // @ts-expect-error - store[data] = newValue - return store - }) + store[data] = newValue; + return store; + }); }, reset: () => set(defaults), set: (data: SettingsStoreData) => set(data), - } -} + }; +}; -export const settingsStore = createSequenceStore() +export const settingsStore = createSequenceStore(); diff --git a/client/src/lib/utils/getVoicePath.ts b/client/src/lib/utils/getVoicePath.ts index 6a1178f..750a2a1 100644 --- a/client/src/lib/utils/getVoicePath.ts +++ b/client/src/lib/utils/getVoicePath.ts @@ -1,3 +1,6 @@ +import { get } from "svelte/store"; +import { settingsStore } from "../stores/settingsStore"; + /** * Retrieves the voice audio path for the given audio file. * @@ -5,7 +8,18 @@ * @param lang - the language of the audio * @return the path of the audio file */ -type SupportedLanguage = 'en' | 'rus' -export default function getVoicePath(audio: string, lang: SupportedLanguage) { - return `/static/voices/${lang}/${audio}.wav` +type SupportedLanguage = "en" | "rus"; + +let currentLang = "en"; + +settingsStore.subscribe((data) => { + currentLang = data.voiceLang; +}); +export default function getVoicePath(audio: string, lang?: SupportedLanguage) { + console.log(get(settingsStore).voiceLang); + if (!lang) { + return `/static/voices/${get(settingsStore).voiceLang}/${audio}.wav`; + } + + return `/static/voices/${lang}/${audio}.wav`; }