Merge branch 'camera-overhaul' into camera-overhaul-update
This commit is contained in:
commit
b5fa64d40f
14 changed files with 803 additions and 290 deletions
21
README.md
21
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
|
||||
|
|
152
client/package-lock.json
generated
152
client/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
16
client/src/lib/Dashboard/Visualization/CameraControls/CameraControls.svelte.d.ts
vendored
Normal file
16
client/src/lib/Dashboard/Visualization/CameraControls/CameraControls.svelte.d.ts
vendored
Normal 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
|
||||
> {}
|
|
@ -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
|
||||
/>
|
|
@ -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>();
|
|
@ -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),
|
||||
})
|
||||
}
|
|
@ -1,23 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { T, useTask } from '@threlte/core'
|
||||
import { ContactShadows, Float, Grid, OrbitControls } from '@threlte/extras'
|
||||
import Robot from './models/RobotDecimated.svelte'
|
||||
import Controls from './Controls.svelte'
|
||||
import { T, useTask } from "@threlte/core";
|
||||
import { ContactShadows, Float, Grid, OrbitControls } from "@threlte/extras";
|
||||
import Controls from "./Controls.svelte";
|
||||
|
||||
import {
|
||||
Vector3,
|
||||
type Camera,
|
||||
type Group,
|
||||
type Object3D,
|
||||
type Object3DEventMap,
|
||||
} from 'three'
|
||||
} from "three";
|
||||
import {
|
||||
telemetryReadonlyStore,
|
||||
telemetryStore,
|
||||
} from '../../stores/telemetryStore'
|
||||
import { get } from 'svelte/store'
|
||||
import { Vector2 } from 'three'
|
||||
import { SmoothMotionController } from './smoothMotionController'
|
||||
import { onMount } from 'svelte'
|
||||
} from "../../stores/telemetryStore";
|
||||
import { get } from "svelte/store";
|
||||
import { Vector2 } from "three";
|
||||
import { SmoothMotionController } from "./smoothMotionController";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
/* 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
|
||||
|
@ -32,91 +32,91 @@
|
|||
is the most esoteric and jank code ever written.
|
||||
*/
|
||||
|
||||
let shouldOrbit = true
|
||||
let shouldOrbit = true;
|
||||
|
||||
// CONSTANTS
|
||||
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 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
|
||||
|
||||
// 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
|
||||
let targetRot = 0
|
||||
let targetRot = 0;
|
||||
|
||||
// Updates rotation to match target with PID controller (intended to be invoked in useTask)
|
||||
let rot = 0 // (initial) rotation in radians
|
||||
let angularVelocity = 0
|
||||
let rot = 0; // (initial) rotation in radians
|
||||
let angularVelocity = 0;
|
||||
const updateRotation = (delta: number) => {
|
||||
let angleDifference = targetRot - rot
|
||||
let angleDifference = targetRot - rot;
|
||||
|
||||
// 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
|
||||
let desiredVelocity =
|
||||
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 (Math.abs(angleDifference) < stoppingThreshold) {
|
||||
desiredVelocity = 0
|
||||
desiredVelocity = 0;
|
||||
}
|
||||
|
||||
// Adjust angular velocity towards desired velocity
|
||||
angularVelocity = desiredVelocity
|
||||
angularVelocity = desiredVelocity;
|
||||
|
||||
// Update rotation
|
||||
rot += angularVelocity * delta
|
||||
rot += angularVelocity * delta;
|
||||
|
||||
// Normalize rot to the range [0, 2π]
|
||||
if (rot < 0) rot += 2 * Math.PI
|
||||
else if (rot > 2 * Math.PI) rot -= 2 * Math.PI
|
||||
if (rot < 0) 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
|
||||
if (Math.abs(angleDifference) < stoppingThreshold) {
|
||||
rot = targetRot
|
||||
angularVelocity = 0
|
||||
rot = targetRot;
|
||||
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 initialVelocity = { x: 0, y: 0 } // Initial velocity
|
||||
const robotPosition = new Vector2(0, 0); // Initial position
|
||||
const initialVelocity = { x: 0, y: 0 }; // Initial velocity
|
||||
// The smooth motion controller utilizes a cubic hermite spline to interpolate between
|
||||
// the current simulation velocity and the robot's actual velocity
|
||||
const controller = new SmoothMotionController(robotPosition, initialVelocity)
|
||||
const controller = new SmoothMotionController(robotPosition, initialVelocity);
|
||||
|
||||
onMount(() => {
|
||||
telemetryReadonlyStore.subscribe(value => {
|
||||
targetRot = (value['orientation'] * Math.PI) / 180 // convert deg to rad
|
||||
telemetryReadonlyStore.subscribe((value) => {
|
||||
targetRot = (value["orientation"] * Math.PI) / 180; // convert deg to rad
|
||||
controller.setTargetVelocity({
|
||||
x: value['chassis-x-speed'],
|
||||
y: value['chassis-y-speed'],
|
||||
})
|
||||
shouldOrbit = value.gear === 'park' || value.gear === '-999'
|
||||
x: value["chassis-x-speed"],
|
||||
y: value["chassis-y-speed"],
|
||||
});
|
||||
shouldOrbit = value.gear === "park" || value.gear === "-999";
|
||||
if (shouldOrbit) {
|
||||
robotPos = new Vector3(0, 0, 0)
|
||||
controller.reset()
|
||||
robotPos = new Vector3(0, 0, 0);
|
||||
controller.reset();
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
useTask(delta => {
|
||||
useTask((delta) => {
|
||||
if (!shouldOrbit) {
|
||||
updateRotation(delta)
|
||||
updateRotation(delta);
|
||||
|
||||
controller.update(delta)
|
||||
robotPos.x = controller.getPosition().x
|
||||
robotPos.z = controller.getPosition().y
|
||||
controller.update(delta);
|
||||
robotPos.x = controller.getPosition().x;
|
||||
robotPos.z = controller.getPosition().y;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let capsule: Group<Object3DEventMap>
|
||||
let capRef: Group<Object3DEventMap>
|
||||
let capsule: Group<Object3DEventMap>;
|
||||
let capRef: Group<Object3DEventMap>;
|
||||
$: if (capsule) {
|
||||
capRef = capsule
|
||||
capRef = capsule;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -145,11 +145,12 @@
|
|||
|
||||
<ContactShadows scale={10} blur={2} far={2.5} opacity={0.5} />
|
||||
|
||||
<Robot
|
||||
position.y={1}
|
||||
|
||||
<!-- <Hornet
|
||||
position.y={2}
|
||||
position.z={robotPos.z}
|
||||
position.x={robotPos.x}
|
||||
scale={[5, 5, 5]}
|
||||
bind:ref={capsule}
|
||||
rotation.y={rot}
|
||||
/>
|
||||
/> -->
|
||||
|
|
|
@ -1,6 +1,39 @@
|
|||
<script lang="ts">
|
||||
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>
|
||||
|
||||
<Canvas>
|
||||
|
|
|
@ -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());
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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<SettingsStoreData>(defaults)
|
||||
const { subscribe, set, update } = writable<SettingsStoreData>(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();
|
||||
|
|
|
@ -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`;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue