Merge branch 'camera-overhaul' into camera-overhaul-update

This commit is contained in:
Youwen Wu 2024-02-27 19:39:13 -08:00 committed by GitHub
commit b5fa64d40f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 803 additions and 290 deletions

View file

@ -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 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). - 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 - Layout, toasts/notifications, music player, and app system ported.
- Media player working with a few small issues - Toast and audio cue system is much more robust
- App system working smoothly - Transitions added almost everywhere to make things smoother
- Camera feed likely working - Tauri app created successfully, currently still using Flask backend
- Frontend syncs basic telemetry data with robot through the same Socket-IO code that powered Jankboard v1 - Visualization vastly improved with Threlte (Three.js) powered 3D robot simulation
- Notification service installed, with Toast and audio capability - 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 ## TODO
- Camera cutout overlay - Camera cutout overlay
- Overhaul audio player system - Overhaul audio player system
- Robot visualization (3D, in Threlte). - Overhaul visualization (especially camera)
- Overhaul backend - Overhaul backend in Rust
- Further integrate telemetry (like GPWS, collision warning, etc) - Further integrate telemetry (like GPWS, collision warning, etc)
- Finish re-creating / adding various voice alerts and sequences - 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

152
client/package-lock.json generated
View file

@ -11,12 +11,14 @@
"@fontsource/roboto": "^5.0.8", "@fontsource/roboto": "^5.0.8",
"@threlte/core": "^7.1.0", "@threlte/core": "^7.1.0",
"@threlte/extras": "^8.8.0", "@threlte/extras": "^8.8.0",
"camera-controls": "^2.8.3",
"howler": "^2.2.4", "howler": "^2.2.4",
"material-icons": "^1.13.12", "material-icons": "^1.13.12",
"material-symbols": "^0.15.0", "material-symbols": "^0.15.0",
"overlayscrollbars-svelte": "^0.5.3", "overlayscrollbars-svelte": "^0.5.3",
"socket.io-client": "^4.7.4", "socket.io-client": "^4.7.4",
"svelte-french-toast": "^1.2.0", "svelte-french-toast": "^1.2.0",
"svelte-tweakpane-ui": "^1.2.1",
"three": "^0.161.0" "three": "^0.161.0"
}, },
"devDependencies": { "devDependencies": {
@ -1035,6 +1037,11 @@
"integrity": "sha512-BRbo1fOtyVbhfLyuCWw6wAWp+U8UQle+ZXu84MYYWzYSEB28dyfnRBIE99eoG+qdAC0po6L2ScIEivcT07UaMA==", "integrity": "sha512-BRbo1fOtyVbhfLyuCWw6wAWp+U8UQle+ZXu84MYYWzYSEB28dyfnRBIE99eoG+qdAC0po6L2ScIEivcT07UaMA==",
"dev": true "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": { "node_modules/@types/cookie": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
@ -1300,6 +1307,14 @@
"node": ">= 6" "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": { "node_modules/caniuse-lite": {
"version": "1.0.30001588", "version": "1.0.30001588",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz",
@ -1592,9 +1607,7 @@
"node_modules/esm-env": { "node_modules/esm-env": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz",
"integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==", "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA=="
"dev": true,
"peer": true
}, },
"node_modules/estree-walker": { "node_modules/estree-walker": {
"version": "3.0.3", "version": "3.0.3",
@ -1604,6 +1617,19 @@
"@types/estree": "^1.0.0" "@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": { "node_modules/fast-glob": {
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@ -2972,6 +2998,17 @@
"svelte": "^3.19.0 || ^4.0.0" "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": { "node_modules/svelte-preprocess": {
"version": "5.1.3", "version": "5.1.3",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.3.tgz", "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": { "node_modules/svelte-writable-derived": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/svelte-writable-derived/-/svelte-writable-derived-3.1.0.tgz", "resolved": "https://registry.npmjs.org/svelte-writable-derived/-/svelte-writable-derived-3.1.0.tgz",

View file

@ -31,12 +31,14 @@
"@fontsource/roboto": "^5.0.8", "@fontsource/roboto": "^5.0.8",
"@threlte/core": "^7.1.0", "@threlte/core": "^7.1.0",
"@threlte/extras": "^8.8.0", "@threlte/extras": "^8.8.0",
"camera-controls": "^2.8.3",
"howler": "^2.2.4", "howler": "^2.2.4",
"material-icons": "^1.13.12", "material-icons": "^1.13.12",
"material-symbols": "^0.15.0", "material-symbols": "^0.15.0",
"overlayscrollbars-svelte": "^0.5.3", "overlayscrollbars-svelte": "^0.5.3",
"socket.io-client": "^4.7.4", "socket.io-client": "^4.7.4",
"svelte-french-toast": "^1.2.0", "svelte-french-toast": "^1.2.0",
"svelte-tweakpane-ui": "^1.2.1",
"three": "^0.161.0" "three": "^0.161.0"
} }
} }

View file

@ -0,0 +1,106 @@
<script context="module" lang="ts">
let installed = false
</script>
<script lang="ts">
import {
T,
forwardEventHandlers,
useTask,
useParent,
useThrelte,
} from '@threlte/core'
import type {
CameraControlsEvents,
CameraControlsProps,
CameraControlsSlots,
} from './CameraControls.svelte'
type $$Props = CameraControlsProps
type $$Events = CameraControlsEvents
type $$Slots = CameraControlsSlots
import CameraControls from 'camera-controls'
import {
Box3,
Matrix4,
Quaternion,
Raycaster,
Sphere,
Spherical,
Vector2,
Vector3,
Vector4,
type PerspectiveCamera,
} from 'three'
import { DEG2RAD } from 'three/src/math/MathUtils'
const subsetOfTHREE = {
Vector2,
Vector3,
Vector4,
Quaternion,
Matrix4,
Spherical,
Box3,
Sphere,
Raycaster,
}
if (!installed) {
CameraControls.install({ THREE: subsetOfTHREE })
installed = true
}
const parent = useParent()
if (!$parent) {
throw new Error('CameraControls must be a child of a ThreeJS camera')
}
const { renderer, invalidate } = useThrelte()
export let autoRotate = false
export let autoRotateSpeed = 1
export const ref = new CameraControls(
$parent as PerspectiveCamera,
renderer?.domElement
)
const getControls = () => ref
let disableAutoRotate = false
useTask(
delta => {
if (autoRotate && !disableAutoRotate) {
getControls().azimuthAngle += 4 * delta * DEG2RAD * autoRotateSpeed
}
const updated = getControls().update(delta)
if (updated) invalidate()
},
{
autoInvalidate: false,
}
)
const forwardingComponent = forwardEventHandlers()
</script>
<T
is={ref}
on:controlstart={e => {
disableAutoRotate = true
}}
on:zoom={e => {
console.log('zoomstart', e)
}}
on:controlend={() => {
disableAutoRotate = false
}}
{...$$restProps}
bind:this={$forwardingComponent}
>
<slot {ref} />
</T>

View file

@ -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<CC> & {
autoRotate?: boolean
autoRotateSpeed?: number
}
export type CameraControlsEvents = Events<CC>
export type CameraControlsSlots = Slots<CC>
export default class CameraControls extends SvelteComponent<
CameraControlsProps,
CameraControlsEvents,
CameraControlsSlots
> {}

View file

@ -0,0 +1,122 @@
<script lang="ts">
import { T, useTask } from "@threlte/core";
import { Grid } from "@threlte/extras";
import CameraControls from "./CameraControls.svelte";
import { cameraControls, mesh } from "./utils/cameraStore";
import { Vector3 } from "three";
import { onMount } from "svelte";
import RobotDecimated from "../models/RobotDecimated.svelte";
function vectorFromObject() {
let ideal: Vector3 = new Vector3();
$cameraControls.getPosition(ideal, true);
ideal.applyQuaternion($mesh.quaternion);
ideal.add(
new Vector3($mesh.position.x, $mesh.position.y, $mesh.position.z)
);
return ideal;
}
const follow = (delta: number) => {
// the object's position is bound to the prop
if (!$mesh || !$cameraControls) return;
// typescript HACKS! never do this! How does this work? who knows!
const robotPosition = vectorFromObject();
const horizontalOffsetDistance = 12; // Distance behind the leading vector
const direction = new Vector3(0, 0, 1); // Default forward direction in Three.js is negative z-axis, so behind is positive z-axis
const verticalOffset = new Vector3(0, -2.8, 0);
// Calculate the offset vector
const offsetVector = direction
.normalize()
.multiplyScalar(horizontalOffsetDistance)
.add(verticalOffset);
// If the leading object is rotating, apply its rotation to the offset vector
const rotatedOffsetVector = offsetVector.applyQuaternion($mesh.quaternion);
// Calculate the trailing vector's position
const trailingVector = robotPosition.clone().sub(rotatedOffsetVector);
$cameraControls.setLookAt(
trailingVector.x,
trailingVector.y,
trailingVector.z,
$mesh.position.x,
$mesh.position.y,
$mesh.position.z,
true
);
};
useTask((delta) => {
// follow(delta)
// $cameraControls.moveTo(
// $mesh.position.x,
// $mesh.position.y,
// $mesh.position.z,
// true
// )
});
onMount(() => {
setTimeout(() => {
// $cameraControls.setLookAt(
// $mesh.position.x,
// $mesh.position.y,
// $mesh.position.z,
// $mesh.position.x,
// $mesh.position.y,
// $mesh.position.z,
// true
// )
$cameraControls.setLookAt(
30,
20,
-40,
$mesh.position.x,
$mesh.position.y,
$mesh.position.z,
true
);
}, 8000);
});
</script>
<T.PerspectiveCamera
makeDefault
position={[10, 10, 10]}
on:create={({ ref }) => {
ref.lookAt(0, 1, 0);
}}
>
<CameraControls
on:create={({ ref }) => {
$cameraControls = ref;
}}
/>
</T.PerspectiveCamera>
<T.DirectionalLight position={[3, 10, 7]} />
<RobotDecimated
scale={[10, 10, 10]}
position.y={0}
on:create={({ ref }) => {
// @ts-expect-error
$mesh = ref;
}}
/>
<Grid
sectionColor={"#ff3e00"}
sectionThickness={1}
fadeDistance={100}
cellSize={6}
sectionSize={24}
cellColor={"#cccccc"}
infiniteGrid
/>

View file

@ -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<CameraControls>();
export const mesh = writable<Mesh>();

View file

@ -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<OrbitControls | undefined>
}
/**
* ### `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<ControlsContext>('threlte-controls', {
orbitControls: writable<OrbitControls | undefined>(undefined),
})
}

View file

@ -1,23 +1,23 @@
<script lang="ts"> <script lang="ts">
import { T, useTask } from '@threlte/core' import { T, useTask } from "@threlte/core";
import { ContactShadows, Float, Grid, OrbitControls } from '@threlte/extras' import { ContactShadows, Float, Grid, OrbitControls } from "@threlte/extras";
import Robot from './models/RobotDecimated.svelte' import Controls from "./Controls.svelte";
import Controls from './Controls.svelte'
import { import {
Vector3, Vector3,
type Camera, type Camera,
type Group, type Group,
type Object3D, type Object3D,
type Object3DEventMap, type Object3DEventMap,
} from 'three' } from "three";
import { import {
telemetryReadonlyStore, telemetryReadonlyStore,
telemetryStore, telemetryStore,
} from '../../stores/telemetryStore' } from "../../stores/telemetryStore";
import { get } from 'svelte/store' import { get } from "svelte/store";
import { Vector2 } from 'three' import { Vector2 } from "three";
import { SmoothMotionController } from './smoothMotionController' import { SmoothMotionController } from "./smoothMotionController";
import { onMount } from 'svelte' import { onMount } from "svelte";
/* This is the root scene where the robot visualization is built. /* This is the root scene where the robot visualization is built.
It renders an infinite grid (it's not actually infinite, but we shouldn't run out It renders an infinite grid (it's not actually infinite, but we shouldn't run out
@ -32,91 +32,91 @@
is the most esoteric and jank code ever written. is the most esoteric and jank code ever written.
*/ */
let shouldOrbit = true let shouldOrbit = true;
// CONSTANTS // CONSTANTS
const maxAngularVelocity = 2 // Max angular velocity, in radians per second const maxAngularVelocity = 2; // Max angular velocity, in radians per second
const stoppingThreshold = 0.005 // Threshold in radians for when to consider the rotation close enough to stop const stoppingThreshold = 0.005; // Threshold in radians for when to consider the rotation close enough to stop
// Proportional control factor // Proportional control factor
const kP = 2 // Adjust this value based on responsiveness and stability needs const kP = 2; // Adjust this value based on responsiveness and stability needs
// Sync robot orientation with target rotation // Sync robot orientation with target rotation
let targetRot = 0 let targetRot = 0;
// Updates rotation to match target with PID controller (intended to be invoked in useTask) // Updates rotation to match target with PID controller (intended to be invoked in useTask)
let rot = 0 // (initial) rotation in radians let rot = 0; // (initial) rotation in radians
let angularVelocity = 0 let angularVelocity = 0;
const updateRotation = (delta: number) => { const updateRotation = (delta: number) => {
let angleDifference = targetRot - rot let angleDifference = targetRot - rot;
// Normalize angle difference to the range [-π, π] // Normalize angle difference to the range [-π, π]
angleDifference = ((angleDifference + Math.PI) % (2 * Math.PI)) - Math.PI angleDifference = ((angleDifference + Math.PI) % (2 * Math.PI)) - Math.PI;
// Calculate the desired angular velocity based on the angle difference // Calculate the desired angular velocity based on the angle difference
let desiredVelocity = let desiredVelocity =
Math.sign(angleDifference) * Math.sign(angleDifference) *
Math.min(maxAngularVelocity, Math.abs(kP * angleDifference)) Math.min(maxAngularVelocity, Math.abs(kP * angleDifference));
// If the object is very close to the target, adjust the desired velocity to zero to prevent overshooting // If the object is very close to the target, adjust the desired velocity to zero to prevent overshooting
if (Math.abs(angleDifference) < stoppingThreshold) { if (Math.abs(angleDifference) < stoppingThreshold) {
desiredVelocity = 0 desiredVelocity = 0;
} }
// Adjust angular velocity towards desired velocity // Adjust angular velocity towards desired velocity
angularVelocity = desiredVelocity angularVelocity = desiredVelocity;
// Update rotation // Update rotation
rot += angularVelocity * delta rot += angularVelocity * delta;
// Normalize rot to the range [0, 2π] // Normalize rot to the range [0, 2π]
if (rot < 0) rot += 2 * Math.PI if (rot < 0) rot += 2 * Math.PI;
else if (rot > 2 * Math.PI) rot -= 2 * Math.PI else if (rot > 2 * Math.PI) rot -= 2 * Math.PI;
// Snap to the target rotation to prevent tiny oscillations if close enough // Snap to the target rotation to prevent tiny oscillations if close enough
if (Math.abs(angleDifference) < stoppingThreshold) { if (Math.abs(angleDifference) < stoppingThreshold) {
rot = targetRot rot = targetRot;
angularVelocity = 0 angularVelocity = 0;
} }
} };
let robotPos: Vector3 = new Vector3(0, 0, 0) let robotPos: Vector3 = new Vector3(0, 0, 0);
const robotPosition = new Vector2(0, 0) // Initial position const robotPosition = new Vector2(0, 0); // Initial position
const initialVelocity = { x: 0, y: 0 } // Initial velocity const initialVelocity = { x: 0, y: 0 }; // Initial velocity
// The smooth motion controller utilizes a cubic hermite spline to interpolate between // The smooth motion controller utilizes a cubic hermite spline to interpolate between
// the current simulation velocity and the robot's actual velocity // the current simulation velocity and the robot's actual velocity
const controller = new SmoothMotionController(robotPosition, initialVelocity) const controller = new SmoothMotionController(robotPosition, initialVelocity);
onMount(() => { onMount(() => {
telemetryReadonlyStore.subscribe(value => { telemetryReadonlyStore.subscribe((value) => {
targetRot = (value['orientation'] * Math.PI) / 180 // convert deg to rad targetRot = (value["orientation"] * Math.PI) / 180; // convert deg to rad
controller.setTargetVelocity({ controller.setTargetVelocity({
x: value['chassis-x-speed'], x: value["chassis-x-speed"],
y: value['chassis-y-speed'], y: value["chassis-y-speed"],
}) });
shouldOrbit = value.gear === 'park' || value.gear === '-999' shouldOrbit = value.gear === "park" || value.gear === "-999";
if (shouldOrbit) { if (shouldOrbit) {
robotPos = new Vector3(0, 0, 0) robotPos = new Vector3(0, 0, 0);
controller.reset() controller.reset();
} }
}) });
}) });
useTask(delta => { useTask((delta) => {
if (!shouldOrbit) { if (!shouldOrbit) {
updateRotation(delta) updateRotation(delta);
controller.update(delta) controller.update(delta);
robotPos.x = controller.getPosition().x robotPos.x = controller.getPosition().x;
robotPos.z = controller.getPosition().y robotPos.z = controller.getPosition().y;
} }
}) });
let capsule: Group<Object3DEventMap> let capsule: Group<Object3DEventMap>;
let capRef: Group<Object3DEventMap> let capRef: Group<Object3DEventMap>;
$: if (capsule) { $: if (capsule) {
capRef = capsule capRef = capsule;
} }
</script> </script>
@ -145,11 +145,12 @@
<ContactShadows scale={10} blur={2} far={2.5} opacity={0.5} /> <ContactShadows scale={10} blur={2} far={2.5} opacity={0.5} />
<Robot
position.y={1} <!-- <Hornet
position.y={2}
position.z={robotPos.z} position.z={robotPos.z}
position.x={robotPos.x} position.x={robotPos.x}
scale={[5, 5, 5]} scale={[5, 5, 5]}
bind:ref={capsule} bind:ref={capsule}
rotation.y={rot} rotation.y={rot}
/> /> -->

View file

@ -1,6 +1,39 @@
<script lang="ts"> <script lang="ts">
import { Canvas } from '@threlte/core' import { Canvas } from '@threlte/core'
import Scene from './Scene.svelte' // @ts-expect-error
import { DEG2RAD } from 'three/src/math/MathUtils'
import { Pane, Button, Separator } from 'svelte-tweakpane-ui'
import { cameraControls, mesh } from './CameraControls/utils/cameraStore'
import Scene from './CameraControls/Scene.svelte'
import { onMount } from 'svelte'
import {
Vector3,
type OrthographicCamera,
type PerspectiveCamera,
} from 'three'
let camera: PerspectiveCamera | OrthographicCamera
$: if ($cameraControls) {
// @ts-expect-error
camera = $cameraControls._camera
}
onMount(() => {
// setTimeout(() => {
// // $cameraControls.moveTo(3, 5, 2, true)
// // $cameraControls.setOrbitPoint(20, 20, 20)
// $cameraControls.setLookAt(
// 3,
// 5,
// 2,
// $mesh.position.x,
// $mesh.position.y,
// $mesh.position.z,
// true
// )
// }, 8000)
})
</script> </script>
<Canvas> <Canvas>

View file

@ -1,113 +1,147 @@
import { toast } from 'svelte-french-toast' import { toast } from "svelte-french-toast";
import type { ToastOptions } from 'svelte-french-toast' import type { ToastOptions } from "svelte-french-toast";
import InfoIcon from './InfoIcon.svelte' import InfoIcon from "./InfoIcon.svelte";
import { Howl } from 'howler' import { Howl } from "howler";
import WarnIcon from './WarnIcon.svelte' import WarnIcon from "./WarnIcon.svelte";
interface NotificationOptions extends ToastOptions { interface NotificationOptions extends ToastOptions {
withAudio?: boolean withAudio?: boolean;
src?: string src?: string;
onComplete?: () => void onComplete?: () => void;
} }
// get colors from https://tailwindcss.com/docs/customizing-colors // get colors from https://tailwindcss.com/docs/customizing-colors
export class Notifications { export class Notifications {
private static readonly defaultDuration = 3000 private static readonly defaultDuration = 3000;
public static success(message: string, options?: NotificationOptions) { public static success(message: string, options?: NotificationOptions) {
if (options?.withAudio && !options.src) if (options?.withAudio && !options.src)
throw new Error('No audio source provided') throw new Error("No audio source provided");
const onComplete = () => { const onComplete = () => {
if (options?.onComplete) options.onComplete() if (options?.onComplete) options.onComplete();
} };
const sendToast = (duration: number) => { const sendToast = (duration: number) => {
toast.success(message, { toast.success(message, {
style: 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, duration,
...options, ...options,
}) });
} };
if (options?.withAudio && options?.src) { if (options?.withAudio && options?.src) {
let sound: Howl let sound: Howl;
sound = new Howl({ sound = new Howl({
src: [options.src], src: [options.src],
preload: true, preload: true,
autoplay: true, autoplay: true,
onload: () => { onload: () => {
let duration = sound.duration() * 1000 let duration = sound.duration() * 1000;
sendToast(duration) sendToast(duration);
setTimeout(onComplete, duration) setTimeout(onComplete, duration);
}, },
}) });
} else { } else {
sendToast(this.defaultDuration) sendToast(this.defaultDuration);
setTimeout(onComplete, 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) { public static info(message: string, options?: NotificationOptions) {
const onComplete = () => { const onComplete = () => {
if (options?.onComplete) options.onComplete() if (options?.onComplete) options.onComplete();
} };
const sendToast = (duration: number) => { const sendToast = (duration: number) => {
toast(message, { toast(message, {
style: 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, icon: InfoIcon,
duration, duration,
...options, ...options,
}) });
} };
if (options?.withAudio && options?.src) { if (options?.withAudio && options?.src) {
let sound: Howl let sound: Howl;
sound = new Howl({ sound = new Howl({
src: [options.src], src: [options.src],
preload: true, preload: true,
autoplay: true, autoplay: true,
onload: () => { onload: () => {
let duration = sound.duration() * 1000 let duration = sound.duration() * 1000;
sendToast(duration) sendToast(duration);
setTimeout(onComplete, duration) setTimeout(onComplete, duration);
}, },
}) });
} else { } else {
sendToast(this.defaultDuration) sendToast(this.defaultDuration);
setTimeout(onComplete, this.defaultDuration) setTimeout(onComplete, this.defaultDuration);
} }
} }
public static warn(message: string, options?: NotificationOptions) { public static warn(message: string, options?: NotificationOptions) {
const onComplete = () => { const onComplete = () => {
if (options?.onComplete) options.onComplete() if (options?.onComplete) options.onComplete();
} };
const sendToast = (duration: number) => { const sendToast = (duration: number) => {
toast(message, { toast(message, {
style: 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, icon: WarnIcon,
duration, duration,
...options, ...options,
}) });
} };
if (options?.withAudio && options?.src) { if (options?.withAudio && options?.src) {
let sound: Howl let sound: Howl;
sound = new Howl({ sound = new Howl({
src: [options.src], src: [options.src],
preload: true, preload: true,
autoplay: true, autoplay: true,
onload: () => { onload: () => {
let duration = sound.duration() * 1000 let duration = sound.duration() * 1000;
sendToast(duration) sendToast(duration);
setTimeout(onComplete, duration) setTimeout(onComplete, duration);
}, },
}) });
} else { } else {
sendToast(this.defaultDuration) sendToast(this.defaultDuration);
setTimeout(onComplete, this.defaultDuration) setTimeout(onComplete, this.defaultDuration);
} }
} }
public static playAudio(src: string, onComplete: () => void = () => {}) { public static playAudio(src: string, onComplete: () => void = () => {}) {
@ -116,8 +150,8 @@ export class Notifications {
preload: true, preload: true,
autoplay: true, autoplay: true,
onload: () => { onload: () => {
setTimeout(onComplete, 1000 * sound.duration()) setTimeout(onComplete, 1000 * sound.duration());
}, },
}) });
} }
} }

View file

@ -14,47 +14,51 @@ Sequences should be either event-driven or periodic. In the case of periodic
sequences, invoke them in the periodicSequence function sequences, invoke them in the periodicSequence function
*/ */
import { Notifications } from '../Notifications/notifications' import { Notifications } from "../Notifications/notifications";
import { sequenceStore } from '../stores/sequenceStore' import { sequenceStore } from "../stores/sequenceStore";
import { settingsStore } from '../stores/settingsStore' import { settingsStore } from "../stores/settingsStore";
import { get } from 'svelte/store' import { get } from "svelte/store";
import getVoicePath from '../utils/getVoicePath' import getVoicePath from "../utils/getVoicePath";
import { tick } from 'svelte' import { tick } from "svelte";
// await a "tick" (a svelte update frame) at the start of every sequence so that // await a "tick" (a svelte update frame) at the start of every sequence so that
// state is synced and no weird side effects occur // state is synced and no weird side effects occur
export const initializationSequence = async () => { export const initializationSequence = async () => {
await tick() await tick();
Notifications.info('Jankboard initialized!', { Notifications.info("Jankboard initialized!", {
withAudio: true, withAudio: true,
src: getVoicePath('jankboard-initialized', 'en'), src: getVoicePath("jankboard-initialized"),
}) onComplete: () => {
setTimeout(() => { if (get(settingsStore).goWoke) {
if (get(settingsStore).goWoke) return sequenceStore.update("initializationComplete", true);
Notifications.success('LittenOS is online', { periodicSequence();
withAudio: true, return;
src: getVoicePath('littenos-is-online', 'en'), }
}) Notifications.success("LittenOS is online", {
setTimeout(() => {
Notifications.warn('Breaching Monte Vista codebase', {
withAudio: true, withAudio: true,
src: getVoicePath('breaching-monte-vista', 'en'), src: getVoicePath("littenos-is-online"),
}) onComplete: () => {
setTimeout(() => { Notifications.warn("Breaching Monte Vista codebase", {
Notifications.playAudio( withAudio: true,
getVoicePath('hello-virtual-assistant', 'en'), src: getVoicePath("breaching-monte-vista"),
() => { onComplete: () => {
sequenceStore.update('initializationComplete', true) Notifications.playAudio(
periodicSequence() getVoicePath("hello-virtual-assistant"),
} () => {
) sequenceStore.update("initializationComplete", true);
}, 3000) periodicSequence();
}, 3000) }
}, 3000) );
} },
});
},
});
},
});
};
let counter = 1 let counter = 1;
/** /**
* Special sequence that plays invokes itself periodically, started automatically * Special sequence that plays invokes itself periodically, started automatically
* at the end of the initializationSequence * at the end of the initializationSequence
@ -64,7 +68,7 @@ let counter = 1
* @return void * @return void
*/ */
const periodicSequence = async () => { const periodicSequence = async () => {
await tick() await tick();
/** /**
* Returns either true or false based on the provided probability * Returns either true or false based on the provided probability
@ -74,11 +78,11 @@ const periodicSequence = async () => {
*/ */
const chance = (probability: number) => { const chance = (probability: number) => {
if (probability < 0 || probability > 1) { 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. * Calls a callback function at regular intervals.
@ -87,140 +91,141 @@ const periodicSequence = async () => {
* @param callback - the function to call * @param callback - the function to call
*/ */
const every = (seconds: number, callback: () => void) => { const every = (seconds: number, callback: () => void) => {
if (counter % seconds === 0) callback() if (counter % seconds === 0) callback();
} };
// add your periodic sequences here // add your periodic sequences here
every(15, () => { every(15, () => {
if (chance(0.2)) breaching1323Sequence() if (chance(0.2)) breaching1323Sequence();
else if (chance(0.2)) breaching254Sequence() else if (chance(0.2)) breaching254Sequence();
}) });
every(25, () => { every(25, () => {
if (chance(0.05)) bullyingRohanSequence() if (chance(0.05)) bullyingRohanSequence();
else if (chance(0.1)) bypassCoprocessorRestrictionsSequence() else if (chance(0.1)) bypassCoprocessorRestrictionsSequence();
}) });
// Dont touch // Dont touch
counter++ counter++;
setTimeout(periodicSequence, 1000) setTimeout(periodicSequence, 1000);
} };
export const criticalFailureIminentSequence = async () => { export const criticalFailureIminentSequence = async () => {
await tick() await tick();
Notifications.error('Critical robot failure imminent', { Notifications.error("Critical robot failure imminent", {
withAudio: true, withAudio: true,
src: getVoicePath('critical-robot-failure', 'en'), src: getVoicePath("critical-robot-failure"),
}) });
} };
export const collisionDetectedSequence = async () => { export const collisionDetectedSequence = async () => {
await tick() await tick();
Notifications.error('Collision detected', { Notifications.error("Collision detected", {
withAudio: true, withAudio: true,
src: getVoicePath('collision-detected', 'en'), src: getVoicePath("collision-detected"),
}) });
} };
export const collisionImminentSequence = async () => { export const collisionImminentSequence = async () => {
await tick() await tick();
Notifications.error('Collision imminent', { Notifications.error("Collision imminent", {
withAudio: true, withAudio: true,
src: getVoicePath('collision-imminent', 'en'), src: getVoicePath("collision-imminent"),
}) });
} };
export const cruiseControlEngagedSequence = async () => { export const cruiseControlEngagedSequence = async () => {
if (get(settingsStore).disableAnnoyances) return if (get(settingsStore).disableAnnoyances) return;
await tick() await tick();
Notifications.success('Cruise control engaged', { Notifications.success("Cruise control engaged", {
withAudio: true, withAudio: true,
src: getVoicePath('cruise-control-engaged', 'en'), src: getVoicePath("cruise-control-engaged"),
}) });
} };
export const retardSequence = async () => { export const retardSequence = async () => {
if (get(settingsStore).goWoke) return if (get(settingsStore).goWoke) return;
await tick() await tick();
Notifications.warn('Retard', { Notifications.warn("Retard", {
withAudio: true, withAudio: true,
src: getVoicePath('retard', 'en'), src: getVoicePath("retard"),
}) });
} };
const breaching254Sequence = async () => { const breaching254Sequence = async () => {
if (get(settingsStore).disableAnnoyances) return if (get(settingsStore).disableAnnoyances) return;
await tick() await tick();
Notifications.warn('Breaching 254 mainframe', { Notifications.warn("Breaching 254 mainframe", {
withAudio: true, withAudio: true,
src: getVoicePath('breaching-254-mainframe', 'en'), src: getVoicePath("breaching-254-mainframe"),
}) });
} };
const breaching1323Sequence = async () => { const breaching1323Sequence = async () => {
if (get(settingsStore).disableAnnoyances) return if (get(settingsStore).disableAnnoyances) return;
await tick() await tick();
Notifications.warn('Breaching 1323 mainframe', { Notifications.warn("Breaching 1323 mainframe", {
withAudio: true, withAudio: true,
src: getVoicePath('breaching-1323-mainframe', 'en'), src: getVoicePath("breaching-1323-mainframe"),
}) });
} };
const bullyingRohanSequence = async () => { const bullyingRohanSequence = async () => {
if (get(settingsStore).disableAnnoyances) return if (get(settingsStore).disableAnnoyances) return;
await tick() await tick();
Notifications.info('Bullying Rohan', { Notifications.info("Bullying Rohan", {
withAudio: true, withAudio: true,
src: getVoicePath('bullying-rohan', 'en'), src: getVoicePath("bullying-rohan"),
}) });
} };
export const userErrorDetectedSequence = async () => { export const userErrorDetectedSequence = async () => {
await tick() await tick();
Notifications.error('User error detected', { Notifications.error("User error detected", {
withAudio: true, withAudio: true,
src: getVoicePath('user-error-detected', 'en'), src: getVoicePath("user-error-detected"),
}) });
} };
// hacky way to prevent duplicate infotainment bootups // hacky way to prevent duplicate infotainment bootups
let infotainmentStarted = false let infotainmentStarted = false;
export const infotainmentBootupSequence = async () => { export const infotainmentBootupSequence = async () => {
if ( if (
get(sequenceStore).infotainmentStartedFirstTime || get(sequenceStore).infotainmentStartedFirstTime ||
get(settingsStore).disableAnnoyances || get(settingsStore).disableAnnoyances ||
infotainmentStarted infotainmentStarted
) ) {
return return;
}
infotainmentStarted = true infotainmentStarted = true;
await tick() await tick();
const sequence = () => { const sequence = () => {
Notifications.info('Infotainment system buffering', { Notifications.info("Infotainment system buffering", {
withAudio: true, withAudio: true,
src: getVoicePath('infotainment-system-buffering', 'en'), src: getVoicePath("infotainment-system-buffering"),
}) onComplete: () => {
setTimeout(() => { Notifications.success("Infotainment system online", {
Notifications.success('Infotainment system online', { withAudio: true,
withAudio: true, src: getVoicePath("infotainment-system-online"),
src: getVoicePath('infotainment-system-online', 'en'), onComplete: () => {
onComplete: () => { sequenceStore.update("infotainmentStartedFirstTime", true);
sequenceStore.update('infotainmentStartedFirstTime', true) },
}, });
}) },
}, 3000) });
} };
if (!get(sequenceStore).initializationComplete) { if (!get(sequenceStore).initializationComplete) {
const unsubscribe = sequenceStore.subscribe(data => { const unsubscribe = sequenceStore.subscribe((data) => {
if (data.initializationComplete) { if (data.initializationComplete) {
sequence() sequence();
unsubscribe() unsubscribe();
} }
}) });
} else { } else {
sequence() sequence();
} }
} };
/** /**
* Waits for the infotainment system to boot up before executing the given 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) => { const waitForInfotainmentBootup = (sequence: () => void) => {
if (!get(sequenceStore).infotainmentStartedFirstTime) { if (!get(sequenceStore).infotainmentStartedFirstTime) {
const unsubscribe = sequenceStore.subscribe(data => { const unsubscribe = sequenceStore.subscribe((data) => {
if (data.infotainmentStartedFirstTime) { if (data.infotainmentStartedFirstTime) {
sequence() sequence();
unsubscribe() unsubscribe();
} }
}) });
} else { } else {
sequence() sequence();
} }
} };
export const musicPlayerBootupSequence = async () => { export const musicPlayerBootupSequence = async () => {
if ( if (
get(sequenceStore).musicStartedFirstTime || get(sequenceStore).musicStartedFirstTime ||
get(settingsStore).disableAnnoyances get(settingsStore).disableAnnoyances
) )
return return;
await tick() await tick();
sequenceStore.update('musicStartedFirstTime', true) sequenceStore.update("musicStartedFirstTime", true);
waitForInfotainmentBootup(() => { waitForInfotainmentBootup(() => {
Notifications.info('Downloading copyrighted music...', { Notifications.info("Downloading copyrighted music...", {
withAudio: true, withAudio: true,
src: getVoicePath('downloading-copyrighted-music', 'en'), src: getVoicePath("downloading-copyrighted-music"),
}) });
}) });
} };
export const gbaEmulatorBootupSequence = async () => { export const gbaEmulatorBootupSequence = async () => {
if ( if (
get(sequenceStore).gbaEmulatorStartedFirstTime || get(sequenceStore).gbaEmulatorStartedFirstTime ||
get(settingsStore).disableAnnoyances get(settingsStore).disableAnnoyances
) )
return return;
await tick() await tick();
sequenceStore.update('gbaEmulatorStartedFirstTime', true) sequenceStore.update("gbaEmulatorStartedFirstTime", true);
waitForInfotainmentBootup(() => { waitForInfotainmentBootup(() => {
Notifications.info('Loading pirated Nintendo ROMs', { Notifications.info("Loading pirated Nintendo ROMs", {
withAudio: true, withAudio: true,
src: getVoicePath('loading-pirated-nintendo', 'en'), src: getVoicePath("loading-pirated-nintendo"),
}) });
}) });
} };
export const doomBootupSequence = async () => { export const doomBootupSequence = async () => {
if ( if (
get(sequenceStore).doomStartedFirstTime || get(sequenceStore).doomStartedFirstTime ||
get(settingsStore).disableAnnoyances get(settingsStore).disableAnnoyances
) )
return return;
await tick() await tick();
sequenceStore.update('doomStartedFirstTime', true) sequenceStore.update("doomStartedFirstTime", true);
waitForInfotainmentBootup(() => { waitForInfotainmentBootup(() => {
Notifications.success('Doom Engaged', { Notifications.success("Doom Engaged", {
withAudio: true, withAudio: true,
src: getVoicePath('doom-engaged', 'en'), src: getVoicePath("doom-engaged"),
}) });
}) });
} };
const bypassCoprocessorRestrictionsSequence = async () => { const bypassCoprocessorRestrictionsSequence = async () => {
if (get(settingsStore).disableAnnoyances) return if (get(settingsStore).disableAnnoyances) return;
await tick() await tick();
Notifications.warn('Bypassing coprocessor restrictions', { Notifications.warn("Bypassing coprocessor restrictions", {
withAudio: true, withAudio: true,
src: getVoicePath('bypassing-coprocessor-restrictions', 'en'), src: getVoicePath("bypassing-coprocessor-restrictions"),
}) });
} };

View file

@ -1,12 +1,15 @@
/* stores global app wide settings */ /* stores global app wide settings */
import { writable } from 'svelte/store' import { writable } from "svelte/store";
type SupportedLanguage = "en" | "rus";
export interface SettingsStoreData { export interface SettingsStoreData {
disableAnnoyances: boolean disableAnnoyances: boolean;
goWoke: boolean goWoke: boolean;
fastStartup: boolean fastStartup: boolean;
randomWeight: number randomWeight: number;
voiceLang: SupportedLanguage;
} }
export const defaults: SettingsStoreData = { 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 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.) 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) randomWeight: 1, // the weight of random events (multiplied by the original probability)
} voiceLang: "en",
};
const createSequenceStore = () => { const createSequenceStore = () => {
const { subscribe, set, update } = writable<SettingsStoreData>(defaults) const { subscribe, set, update } = writable<SettingsStoreData>(defaults);
return { return {
subscribe, subscribe,
update: ( update: (
data: keyof SettingsStoreData, data: keyof SettingsStoreData,
newValue: SettingsStoreData[typeof data] newValue: SettingsStoreData[typeof data]
) => { ) => {
update(store => { update((store) => {
// @ts-expect-error // @ts-expect-error
store[data] = newValue store[data] = newValue;
return store return store;
}) });
}, },
reset: () => set(defaults), reset: () => set(defaults),
set: (data: SettingsStoreData) => set(data), set: (data: SettingsStoreData) => set(data),
} };
} };
export const settingsStore = createSequenceStore() export const settingsStore = createSequenceStore();

View file

@ -1,3 +1,6 @@
import { get } from "svelte/store";
import { settingsStore } from "../stores/settingsStore";
/** /**
* Retrieves the voice audio path for the given audio file. * Retrieves the voice audio path for the given audio file.
* *
@ -5,7 +8,18 @@
* @param lang - the language of the audio * @param lang - the language of the audio
* @return the path of the audio file * @return the path of the audio file
*/ */
type SupportedLanguage = 'en' | 'rus' type SupportedLanguage = "en" | "rus";
export default function getVoicePath(audio: string, lang: SupportedLanguage) {
return `/static/voices/${lang}/${audio}.wav` 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`;
} }