feat: vastly improve visualization; add more sequences; improve error handling in rust; improve UI; update settings;

This commit is contained in:
Youwen Wu 2024-02-29 23:49:20 -08:00
parent eb41340ee0
commit 05c52d4d4a
18 changed files with 516 additions and 827 deletions

143
client/package-lock.json generated
View file

@ -19,7 +19,6 @@
"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": {
@ -1052,11 +1051,6 @@
"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",
@ -1622,7 +1616,9 @@
"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",
@ -1632,19 +1628,6 @@
"@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",
@ -3013,17 +2996,6 @@
"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",
@ -3087,115 +3059,6 @@
} }
} }
}, },
"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

@ -39,7 +39,6 @@
"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

@ -9,6 +9,8 @@ const NTABLE_IP: (u8, u8, u8, u8) = (10, 12, 80, 2);
const NTABLE_PORT: u16 = 5810; const NTABLE_PORT: u16 = 5810;
pub async fn subscribe_topics(app_handle: AppHandle) { pub async fn subscribe_topics(app_handle: AppHandle) {
loop {
// I hope this doesn't lead to a catastrophic infinite loop failure
let client = loop { let client = loop {
match Client::try_new_w_config( match Client::try_new_w_config(
SocketAddrV4::new( SocketAddrV4::new(
@ -70,3 +72,4 @@ pub async fn subscribe_topics(app_handle: AppHandle) {
.emit_all("telemetry_status", "disconnected") .emit_all("telemetry_status", "disconnected")
.expect("Failed to emit telemetry_disconnected event"); .expect("Failed to emit telemetry_disconnected event");
} }
}

View file

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onDestroy } from 'svelte'
import { import {
cameraControls, cameraControls,
cameraState, cameraState,
@ -11,21 +12,15 @@
setStationaryTelemetry, setStationaryTelemetry,
} from './telemetrySimulators' } from './telemetrySimulators'
let cameraMode = 'orbit' let value: typeof $cameraState.mode = $cameraState.mode
const changeCamera = () => { $: {
if (cameraMode === 'follow-direction') { cameraState.set('mode', value)
cameraMode = 'orbit'
cameraState.set('mode', 'orbit')
cameraState.set('userControlled', false)
console.log($cameraState.mode)
} else {
cameraMode = 'follow-direction'
cameraState.set('mode', 'follow-direction')
cameraState.set('userControlled', true)
console.log($cameraState.mode)
}
} }
const unsubscribe = cameraState.subscribe(state => {
if (value !== state.mode) value = state.mode
})
onDestroy(unsubscribe)
</script> </script>
<AppContainer <AppContainer
@ -69,7 +64,11 @@
<button class="button" on:click={simulateMotion}> <button class="button" on:click={simulateMotion}>
Simulate random motion Simulate random motion
</button> </button>
<button class="button" on:click={changeCamera}> Change camera mode </button> <select bind:value class="bg-slate-300">
<option value="orbit">Orbit</option>
<option value="follow-facing">Follow Facing</option>
<option value="follow-direction">Follow Direction</option>
</select>
</AppContainer> </AppContainer>
<style lang="postcss"> <style lang="postcss">

View file

@ -47,6 +47,11 @@
tooltip="Selects the language/locale used for Jankboard voice prompts. Does not affect application language (ie. Jankboard itself will always be in English)." tooltip="Selects the language/locale used for Jankboard voice prompts. Does not affect application language (ie. Jankboard itself will always be in English)."
>Voice Prompt Language</SettingsSelector >Voice Prompt Language</SettingsSelector
> >
<SettingsToggle
setting="sentry"
tooltip="Sentry mode protects the robot and operator from foreign threats."
>Sentry Mode</SettingsToggle
>
<button <button
class="mt-10 px-4 py-2 bg-amber-600 hover:brightness-75 text-medium rounded-lg w-min" class="mt-10 px-4 py-2 bg-amber-600 hover:brightness-75 text-medium rounded-lg w-min"
on:click={resetSettings}>Reset</button on:click={resetSettings}>Reset</button

View file

@ -20,7 +20,7 @@
> >
<div class="text-lg font-medium">SPEED<br />LIMIT</div> <div class="text-lg font-medium">SPEED<br />LIMIT</div>
<div class="text-2xl font-bold transition"> <div class="text-2xl font-bold transition">
{speedLimit} {speedLimit.toFixed(1)}
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,6 +1,40 @@
<script lang="ts"> <script lang="ts">
import {
shiftedInAutoSequence,
shiftedInDriveSequence,
shiftedInLowSequence,
shiftedInNeutralSequence,
shiftedInParkSequence,
shiftedInReverseSequence,
} from '../../Sequences/sequences'
export let selectedGear: Gear export let selectedGear: Gear
export let placeholder: boolean export let placeholder: boolean
const shift = (selectedGear: Gear) => {
switch (selectedGear) {
case 'park':
shiftedInParkSequence()
break
case 'reverse':
shiftedInReverseSequence()
break
case 'neutral':
shiftedInNeutralSequence()
break
case 'low':
shiftedInLowSequence()
break
case 'auto':
shiftedInAutoSequence()
break
case 'drive':
shiftedInDriveSequence()
break
}
}
$: shift(selectedGear)
</script> </script>
<div class="flex justify-center w-full transition"> <div class="flex justify-center w-full transition">

View file

@ -7,28 +7,39 @@
Displays the drive mode Displays the drive mode
--> -->
<script lang="ts"> <script lang="ts">
import { fade } from 'svelte/transition' import {
modeChillSequence,
modeCruiseSequence,
modeLudicrousSequence,
} from '../../Sequences/sequences'
export let selectedMode: Mode export let selectedMode: Mode
export let placeholder: boolean export let placeholder: boolean
let modeText = '' let modeText = ''
const setModeText = (selectedMode: Mode) => {
switch (selectedMode) {
case 'chill':
modeText = 'CHILL'
modeChillSequence()
break
case 'cruise':
modeText = 'CRUISE'
modeCruiseSequence()
break
case 'ludicrous':
modeText = 'LUDICROUS'
modeLudicrousSequence()
break
}
}
$: { $: {
if (placeholder) { if (placeholder) {
modeText = 'DISCONNECTED' modeText = 'DISCONNECTED'
} else { } else {
switch (selectedMode) { setModeText(selectedMode)
case 'chill':
modeText = 'CHILL'
break
case 'cruise':
modeText = 'CRUISE'
break
case 'ludicrous':
modeText = 'LUDICROUS'
break
}
} }
} }
</script> </script>

View file

@ -33,6 +33,7 @@
Vector4, Vector4,
type PerspectiveCamera, type PerspectiveCamera,
} from 'three' } from 'three'
// @ts-expect-error
import { DEG2RAD } from 'three/src/math/MathUtils' import { DEG2RAD } from 'three/src/math/MathUtils'
import { cameraState } from './utils/cameraStore' import { cameraState } from './utils/cameraStore'
@ -61,7 +62,7 @@
const { renderer, invalidate } = useThrelte() const { renderer, invalidate } = useThrelte()
export let autoRotate = false let autoRotate = true
export let autoRotateSpeed = 1 export let autoRotateSpeed = 1
export const ref = new CameraControls( export const ref = new CameraControls(
@ -73,7 +74,7 @@
useTask( useTask(
delta => { delta => {
if (autoRotate && !$cameraState.userControlled) { if (autoRotate && $cameraState.mode === 'orbit') {
getControls().azimuthAngle += 4 * delta * DEG2RAD * autoRotateSpeed getControls().azimuthAngle += 4 * delta * DEG2RAD * autoRotateSpeed
} }
const updated = getControls().update(delta) const updated = getControls().update(delta)
@ -90,13 +91,10 @@
<T <T
is={ref} is={ref}
on:controlstart={e => { on:controlstart={e => {
cameraState.set('userControlled', true) autoRotate = false
}}
on:zoom={e => {
console.log('zoomstart', e)
}} }}
on:controlend={() => { on:controlend={() => {
cameraState.set('userControlled', false) autoRotate = true
}} }}
{...$$restProps} {...$$restProps}
bind:this={$forwardingComponent} bind:this={$forwardingComponent}

View file

@ -1,96 +0,0 @@
<script lang="ts">
import { T, useTask } from '@threlte/core'
import { Grid } from '@threlte/extras'
import CameraControls from './CameraControls.svelte'
import { cameraControls, mesh, cameraState } from './utils/cameraStore'
import { Vector3 } from 'three'
import { onMount } from 'svelte'
import RobotDecimated from '../models/RobotDecimated.svelte'
import { telemetryReadonlyStore } from '../../../stores/telemetryStore'
import { DEG2RAD } from 'three/src/math/MathUtils.js'
const SPEED_MULTIPLIER = 4
const axis = new Vector3(0, 1, 0)
const follow = (delta: number) => {
// the object's position is bound to the prop
if (!$mesh || !$cameraControls) return
const offsetPosition = new Vector3()
offsetPosition.copy($mesh.position)
const offsetVector = new Vector3(2.5, 0, -2)
offsetVector.applyAxisAngle(axis, $mesh.rotation.y)
offsetPosition.add(offsetVector)
cameraState.set('mode', 'follow-direction')
if (($cameraState.mode = 'follow-facing')) {
$cameraControls.setLookAt(
offsetPosition.x + 15 * Math.sin($mesh.rotation.y),
offsetPosition.y + 8,
offsetPosition.z + 15 * Math.cos($mesh.rotation.y),
offsetPosition.x,
offsetPosition.y,
offsetPosition.z,
true
)
}
if (($cameraState.mode = 'orbit')) {
$cameraControls.moveTo(
offsetPosition.x,
offsetPosition.y,
offsetPosition.z
// true
)
}
}
useTask(delta => {
$mesh.position.x +=
$telemetryReadonlyStore['chassis-y-speed'] * delta * SPEED_MULTIPLIER
$mesh.position.z +=
$telemetryReadonlyStore['chassis-x-speed'] * delta * SPEED_MULTIPLIER
$mesh.rotation.y = $telemetryReadonlyStore.orientation * DEG2RAD
follow(delta)
})
onMount(() => {})
</script>
<T.PerspectiveCamera
makeDefault
position={[12, 10, 12]}
on:create={({ ref }) => {
ref.lookAt(0, 1, 0)
}}
>
<CameraControls
on:create={({ ref }) => {
$cameraControls = ref
}}
autoRotate={$cameraState.mode === 'orbit'}
autoRotateSpeed={3}
/>
</T.PerspectiveCamera>
<T.DirectionalLight position={[3, 10, 7]} />
<T.AmbientLight color={'#f0f0f0'} intensity={0.1} />
<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

@ -14,12 +14,10 @@ type CameraMode =
interface CameraState { interface CameraState {
mode: CameraMode mode: CameraMode
userControlled: boolean
} }
const { set, update, subscribe } = writable<CameraState>({ const { set, update, subscribe } = writable<CameraState>({
mode: 'orbit', mode: 'orbit',
userControlled: false,
}) })
const createCameraState = () => { const createCameraState = () => {
@ -28,7 +26,7 @@ const createCameraState = () => {
subscribe, subscribe,
set: (prop: keyof CameraState, val: any) => set: (prop: keyof CameraState, val: any) =>
update(state => ({ ...state, [prop]: val })), update(state => ({ ...state, [prop]: val })),
reset: () => set({ mode: 'orbit', userControlled: false }), reset: () => set({ mode: 'orbit' }),
} }
} }

View file

@ -1,165 +0,0 @@
<script lang="ts">
import { createEventDispatcher, onDestroy } from 'svelte'
import {
Camera,
Vector2,
Vector3,
Quaternion,
Object3D,
type Object3DEventMap,
Group,
} from 'three'
import { useThrelte, useParent, useTask } from '@threlte/core'
export let object: Group<Object3DEventMap>
export let rotateSpeed = 1.0
export let shouldOrbit: boolean
$: if (object) {
// console.log(object)
// object.position.y = 10
// // Calculate the direction vector towards (0, 0, 0)
// const target = new Vector3(0, 0, 0)
// const direction = target.clone().sub(object.position).normalize()
// // Extract the forward direction from the object's current rotation matrix
// const currentDirection = new Vector3(0, 1, 0)
// currentDirection.applyQuaternion(object.quaternion)
// // Calculate the axis and angle to rotate the object
// const rotationAxis = currentDirection.clone().cross(direction).normalize()
// const rotationAngle = Math.acos(currentDirection.dot(direction))
// // Rotate the object using rotateOnAxis()
// object.rotateOnAxis(rotationAxis, rotationAngle)
}
export let idealOffset = { x: -0.5, y: 2, z: -3 }
export let idealLookAt = { x: 0, y: 1, z: 5 }
const currentPosition = new Vector3()
const currentLookAt = new Vector3()
let isOrbiting = false
let pointerDown = false
const rotateStart = new Vector2()
const rotateEnd = new Vector2()
const rotateDelta = new Vector2()
const axis = new Vector3(0, 1, 0)
const rotationQuat = new Quaternion()
const { renderer, invalidate } = useThrelte()
const domElement = renderer.domElement
const camera = useParent()
const dispatch = createEventDispatcher()
const isCamera = (p: any): p is Camera => {
return p.isCamera
}
if (!isCamera($camera)) {
throw new Error(
'Parent missing: <PointerLockControls> need to be a child of a <Camera>'
)
}
// This is basically your update function
useTask(delta => {
// the object's position is bound to the prop
if (!object) return
// then we calculate our ideal's
const offset = vectorFromObject(idealOffset)
const lookAt = vectorFromObject(idealLookAt)
// camera is based on character so we rotation character first
// rotationQuat.setFromAxisAngle(axis, rotateSpeed * delta)
// object.quaternion.multiply(rotationQuat)
// and how far we should move towards them
const t = 1.0 - Math.pow(0.001, delta)
currentPosition.lerp(offset, t)
currentLookAt.lerp(lookAt, t)
// typescript HACKS! never do this! How does this work? who knows!
const robotPosition = vectorFromObject(
object as unknown as { x: number; y: number; z: number }
)
const horizontalOffsetDistance = 15 // 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, -5, 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(object.quaternion)
const leftOffset = -1.5
function shiftVectorLeft(
vector: Vector3,
amount: number,
upDirection = axis
): void {
// Calculate the left direction. Assuming 'up' is the global up direction or a custom up vector.
// This creates a vector pointing to the "left" of the original vector, in a 3D context.
let leftDirection = new Vector3()
.crossVectors(upDirection, vector)
.normalize()
// Scale the left direction by the desired amount
leftDirection.multiplyScalar(amount)
// Add the scaled left direction to the original vector, mutating it
vector.add(leftDirection)
}
shiftVectorLeft(rotatedOffsetVector, leftOffset)
// Calculate the trailing vector's position
const trailingVector = robotPosition.clone().sub(rotatedOffsetVector)
function shiftVectorLeftNonMutate(
vector: Vector3,
amount: number,
upDirection = axis
): Vector3 {
// Create a new vector to avoid mutating the original vector
let shiftedVector = vector.clone()
// Calculate the left direction. Assuming 'up' is the global up direction or a custom up vector.
// This creates a vector pointing to the "left" of the original vector, in a 3D context.
let leftDirection = new Vector3()
.crossVectors(upDirection, vector)
.normalize()
// Scale the left direction by the desired amount and add it to the original vector
leftDirection.multiplyScalar(amount)
shiftedVector.add(leftDirection)
return shiftedVector
}
if (!shouldOrbit) {
// then finally set the camera, a bit behind the model
$camera!.position.copy(trailingVector)
// Rotate the offset around the Y-axis
$camera!.lookAt(shiftVectorLeftNonMutate(currentLookAt, -leftOffset))
}
})
function vectorFromObject(vec: { x: number; y: number; z: number }) {
const { x, y, z } = vec
const ideal = new Vector3(x, y, z)
ideal.applyQuaternion(object.quaternion)
ideal.add(
new Vector3(object.position.x, object.position.y, object.position.z)
)
return ideal
}
</script>

View file

@ -1,155 +1,134 @@
<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 { Grid } from '@threlte/extras'
import Controls from './Controls.svelte' import CameraControls from './CameraControls/CameraControls.svelte'
import { import {
Vector3, cameraControls,
type Camera, mesh,
type Group, cameraState,
type Object3D, } from './CameraControls/utils/cameraStore'
type Object3DEventMap, import { Vector3 } 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' import { onMount } from 'svelte'
import RobotDecimated from './models/RobotDecimated.svelte'
import { telemetryReadonlyStore } from '../../stores/telemetryStore'
import { DEG2RAD } from 'three/src/math/MathUtils.js'
/* This is the root scene where the robot visualization is built. const SPEED_MULTIPLIER = 4
It renders an infinite grid (it's not actually infinite, but we shouldn't run out const axis = new Vector3(0, 1, 0)
of space in realistic use), and a 3D model of the robot. The camera is locked
to the model, and the model is rotated to match the robot's orientation.
A PID controller is used to smoothly rotate the model to match the robot's
orientation and dampen out jittering. How does it work? Who knows!
75% percent of this was created while reading
https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation,
and rest was generated by AI!
The rest of this codebase is remarkably jank-free, but this visualization module
is the most esoteric and jank code ever written.
*/
let shouldOrbit = true const follow = (delta: number) => {
// the object's position is bound to the prop
if (!$mesh || !$cameraControls) return
// CONSTANTS const offsetPosition = new Vector3()
const maxAngularVelocity = 2 // Max angular velocity, in radians per second offsetPosition.copy($mesh.position)
const stoppingThreshold = 0.005 // Threshold in radians for when to consider the rotation close enough to stop const offsetVector = new Vector3(2.5, 0, -2)
offsetVector.applyAxisAngle(axis, $mesh.rotation.y)
offsetPosition.add(offsetVector)
// Proportional control factor const followDirection = () => {
const kP = 2 // Adjust this value based on responsiveness and stability needs const angle = Math.atan2(
$telemetryReadonlyStore['chassis-y-speed'],
$telemetryReadonlyStore['chassis-x-speed']
)
// Sync robot orientation with target rotation $cameraControls.setLookAt(
let targetRot = 0 offsetPosition.x - 13 * Math.sin(angle),
offsetPosition.y + 4,
// Updates rotation to match target with PID controller (intended to be invoked in useTask) offsetPosition.z - 13 * Math.cos(angle),
let rot = 0 // (initial) rotation in radians offsetPosition.x,
let angularVelocity = 0 offsetPosition.y,
const updateRotation = (delta: number) => { offsetPosition.z,
let angleDifference = targetRot - rot true
)
// Normalize angle difference to the range [-π, π] $cameraControls.zoomTo(1.1, true)
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))
// 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
} }
// Adjust angular velocity towards desired velocity const followFacing = () => {
angularVelocity = desiredVelocity if ($cameraState.mode === 'follow-facing') {
$cameraControls.setLookAt(
// Update rotation offsetPosition.x + 13 * Math.sin($mesh.rotation.y),
rot += angularVelocity * delta offsetPosition.y + 5,
offsetPosition.z + 13 * Math.cos($mesh.rotation.y),
// Normalize rot to the range [0, 2π] offsetPosition.x,
if (rot < 0) rot += 2 * Math.PI offsetPosition.y + 2,
else if (rot > 2 * Math.PI) rot -= 2 * Math.PI offsetPosition.z,
true
// Snap to the target rotation to prevent tiny oscillations if close enough )
if (Math.abs(angleDifference) < stoppingThreshold) {
rot = targetRot
angularVelocity = 0
} }
$cameraControls.zoomTo(1.2, true)
} }
let robotPos: Vector3 = new Vector3(0, 0, 0) const orbit = () => {
$cameraControls.zoomTo(0.8, true)
const robotPosition = new Vector2(0, 0) // Initial position $cameraControls.moveTo(
const initialVelocity = { x: 0, y: 0 } // Initial velocity offsetPosition.x + 4 * Math.sin($mesh.rotation.y),
// The smooth motion controller utilizes a cubic hermite spline to interpolate between offsetPosition.y + 3,
// the current simulation velocity and the robot's actual velocity offsetPosition.z * Math.cos($mesh.rotation.y),
const controller = new SmoothMotionController(robotPosition, initialVelocity) true
)
onMount(() => { }
telemetryReadonlyStore.subscribe(value => {
targetRot = (value['orientation'] * Math.PI) / 180 // convert deg to rad switch ($cameraState.mode) {
controller.setTargetVelocity({ case 'follow-direction':
x: value['chassis-x-speed'], followDirection()
y: value['chassis-y-speed'], break
}) case 'follow-facing':
// shouldOrbit = value.gear === "park" || value.gear === "-999"; followFacing()
if (shouldOrbit) { break
robotPos = new Vector3(0, 0, 0) case 'orbit':
controller.reset() orbit()
break
default:
orbit()
break
}
} }
})
})
useTask(delta => { useTask(delta => {
if (!shouldOrbit) { /* TODO: standardize a scale (meters : grid lengths) so we can have
updateRotation(delta) accurate positioning of sensor detected objects */
// update position data for robot model
$mesh.position.x +=
$telemetryReadonlyStore['chassis-y-speed'] * delta * SPEED_MULTIPLIER
$mesh.position.z +=
$telemetryReadonlyStore['chassis-x-speed'] * delta * SPEED_MULTIPLIER
$mesh.rotation.y = $telemetryReadonlyStore.orientation * DEG2RAD
controller.update(delta) // run the follow function
robotPos.x = controller.getPosition().x follow(delta)
robotPos.z = controller.getPosition().y
}
}) })
let capsule: Group<Object3DEventMap> onMount(() => {})
let capRef: Group<Object3DEventMap>
$: if (capsule) {
capRef = capsule
}
</script> </script>
<T.PerspectiveCamera makeDefault position={[0, 8, -20]} fov={30} on:create> <T.PerspectiveCamera makeDefault position={[12, 10, 12]}>
<OrbitControls <CameraControls
autoRotateSpeed={1.5} on:create={({ ref }) => {
target.y={1.5} $cameraControls = ref
autoRotate }}
enableDamping autoRotateSpeed={3}
/> />
<Controls {shouldOrbit} bind:object={capRef} rotateSpeed={angularVelocity} />
</T.PerspectiveCamera> </T.PerspectiveCamera>
<T.DirectionalLight intensity={0.8} position.x={5} position.y={10} /> <T.DirectionalLight position={[3, 10, 7]} />
<T.AmbientLight intensity={0.2} /> <T.AmbientLight color={'#f0f0f0'} intensity={0.1} />
<Grid <RobotDecimated
position.y={1} scale={[10, 10, 10]}
cellColor="#ffffff" position.y={0}
sectionColor="#ffffff" on:create={({ ref }) => {
sectionThickness={0} // @ts-expect-error
fadeDistance={100} $mesh = ref
cellSize={6} }}
infiniteGrid
/> />
<ContactShadows scale={10} blur={2} far={2.5} opacity={0.5} /> <Grid
sectionColor={'#ff3e00'}
<!-- <Hornet sectionThickness={1}
position.y={2} fadeDistance={100}
position.z={robotPos.z} cellSize={6}
position.x={robotPos.x} sectionSize={24}
scale={[5, 5, 5]} cellColor={'#cccccc'}
bind:ref={capsule} infiniteGrid
rotation.y={rot} />
/> -->

View file

@ -1,16 +1,9 @@
<script lang="ts"> <script lang="ts">
import { Canvas } from '@threlte/core' import { Canvas } from '@threlte/core'
// @ts-expect-error import { cameraControls } from './CameraControls/utils/cameraStore'
import { DEG2RAD } from 'three/src/math/MathUtils' import Scene from './Scene.svelte'
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 { onMount } from 'svelte'
import { import { type OrthographicCamera, type PerspectiveCamera } from 'three'
Vector3,
type OrthographicCamera,
type PerspectiveCamera,
} from 'three'
let camera: PerspectiveCamera | OrthographicCamera let camera: PerspectiveCamera | OrthographicCamera

View file

@ -1,54 +0,0 @@
import { Vector2 } from 'three'
interface Velocity {
x: number
y: number
}
export class SmoothMotionController {
private currentPosition: Vector2
private currentVelocity: Vector2
private targetVelocity: Velocity
private dampingFactor: number
constructor(
initialPosition: Vector2,
initialVelocity: Velocity,
dampingFactor: number = 0.1
) {
this.currentPosition = initialPosition
this.currentVelocity = new Vector2(initialVelocity.x, initialVelocity.y)
this.targetVelocity = { ...initialVelocity }
this.dampingFactor = dampingFactor
}
setTargetVelocity(velocity: Velocity) {
this.targetVelocity = velocity
}
update(delta: number) {
// Apply cubic interpolation to smoothly transition the current velocity towards the target velocity
this.currentVelocity.x +=
(this.targetVelocity.x - this.currentVelocity.x) *
this.dampingFactor *
delta
this.currentVelocity.y +=
(this.targetVelocity.y - this.currentVelocity.y) *
this.dampingFactor *
delta
// Update position based on the current velocity and the time delta
this.currentPosition.x += this.currentVelocity.x * delta * 3
this.currentPosition.y += this.currentVelocity.y * delta * 3
}
getPosition(): Vector2 {
return this.currentPosition
}
public reset() {
this.currentPosition = new Vector2(0, 0)
this.currentVelocity = new Vector2(0, 0)
this.targetVelocity = { x: 0, y: 0 }
}
}

View file

@ -14,51 +14,52 @@ 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'
import { cameraState } from '../Dashboard/Visualization/CameraControls/utils/cameraStore'
// 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"), src: getVoicePath('jankboard-initialized'),
onComplete: () => { onComplete: () => {
if (get(settingsStore).goWoke) { if (get(settingsStore).goWoke) {
sequenceStore.update("initializationComplete", true); sequenceStore.update('initializationComplete', true)
periodicSequence(); periodicSequence()
return; return
} }
Notifications.success("LittenOS is online", { Notifications.success('LittenOS is online', {
withAudio: true, withAudio: true,
src: getVoicePath("littenos-is-online"), src: getVoicePath('littenos-is-online'),
onComplete: () => { onComplete: () => {
Notifications.warn("Breaching Monte Vista codebase", { Notifications.warn('Breaching Monte Vista codebase', {
withAudio: true, withAudio: true,
src: getVoicePath("breaching-monte-vista"), src: getVoicePath('breaching-monte-vista'),
onComplete: () => { onComplete: () => {
Notifications.playAudio( Notifications.playAudio(
getVoicePath("hello-virtual-assistant"), getVoicePath('hello-virtual-assistant'),
() => { () => {
sequenceStore.update("initializationComplete", true); sequenceStore.update('initializationComplete', true)
periodicSequence(); periodicSequence()
} }
); )
}, },
}); })
}, },
}); })
}, },
}); })
}; }
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
@ -68,7 +69,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
@ -78,11 +79,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.
@ -91,141 +92,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"), 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"), 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"), 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"), 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"), 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"), 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"), 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"), 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"), 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"), src: getVoicePath('infotainment-system-buffering'),
onComplete: () => { onComplete: () => {
Notifications.success("Infotainment system online", { Notifications.success('Infotainment system online', {
withAudio: true, withAudio: true,
src: getVoicePath("infotainment-system-online"), src: getVoicePath('infotainment-system-online'),
onComplete: () => { onComplete: () => {
sequenceStore.update("infotainmentStartedFirstTime", true); sequenceStore.update('infotainmentStartedFirstTime', true)
}, },
}); })
}, },
}); })
}; }
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.
@ -236,77 +237,196 @@ 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"), 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"), 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"), src: getVoicePath('doom-engaged'),
}); })
}); })
}; }
const bypassCoprocessorRestrictionsSequence = async () => { const bypassCoprocessorRestrictionsSequence = async () => {
if (get(settingsStore).disableAnnoyances) return; if (
await tick(); get(settingsStore).disableAnnoyances ||
Notifications.warn("Bypassing coprocessor restrictions", { get(sequenceStore).initializationComplete
)
return
await tick()
Notifications.warn('Bypassing coprocessor restrictions', {
withAudio: true, withAudio: true,
src: getVoicePath("bypassing-coprocessor-restrictions"), src: getVoicePath('bypassing-coprocessor-restrictions'),
}); })
}; }
export const shiftedInParkSequence = async () => {
await tick()
cameraState.set('mode', 'orbit')
if (
get(settingsStore).disableAnnoyances ||
!get(sequenceStore).initializationComplete
)
return
Notifications.playAudio(getVoicePath('parked-brakes-engaged'), () => {
if (!get(settingsStore).sentry) return
Notifications.playAudio(getVoicePath('sentry-mode-engaged'))
Notifications.warn('Sentry mode engaged. Threats will be neutralized')
})
}
export const shiftedInReverseSequence = async () => {
await tick()
cameraState.set('mode', 'follow-direction')
if (
get(settingsStore).disableAnnoyances ||
!get(sequenceStore).initializationComplete
)
return
Notifications.playAudio(getVoicePath('reverse'))
}
export const shiftedInNeutralSequence = async () => {
await tick()
cameraState.set('mode', 'orbit')
if (
get(settingsStore).disableAnnoyances ||
!get(sequenceStore).initializationComplete
)
return
Notifications.playAudio(getVoicePath('neutral-brakes-engaged'))
}
export const shiftedInLowSequence = async () => {
await tick()
cameraState.set('mode', 'follow-facing')
if (
get(settingsStore).disableAnnoyances ||
!get(sequenceStore).initializationComplete
)
return
Notifications.playAudio(getVoicePath('shifted-into-low'))
}
export const shiftedInAutoSequence = async () => {
await tick()
cameraState.set('mode', 'follow-direction')
if (
get(settingsStore).disableAnnoyances ||
!get(sequenceStore).initializationComplete
)
return
Notifications.playAudio(getVoicePath('shifted-into-automatic'))
}
export const shiftedInDriveSequence = async () => {
await tick()
cameraState.set('mode', 'follow-facing')
if (
get(settingsStore).disableAnnoyances ||
!get(sequenceStore).initializationComplete
)
return
Notifications.playAudio(getVoicePath('shifted-into-drive'))
}
export const modeChillSequence = async () => {
if (
get(settingsStore).disableAnnoyances ||
!get(sequenceStore).initializationComplete
)
return
await tick()
Notifications.playAudio(getVoicePath('set-acceleration-profile-chill'))
}
export const modeCruiseSequence = async () => {
if (
get(settingsStore).disableAnnoyances ||
!get(sequenceStore).initializationComplete
)
return
await tick()
Notifications.playAudio(getVoicePath('cruise-control-engaged'))
}
export const modeLudicrousSequence = async () => {
if (
get(settingsStore).disableAnnoyances ||
!get(sequenceStore).initializationComplete
)
return
await tick()
Notifications.playAudio(getVoicePath('set-acceleration-profile-ludicrous'))
}

View file

@ -10,6 +10,7 @@ export interface SettingsStoreData {
fastStartup: boolean fastStartup: boolean
randomWeight: number randomWeight: number
voiceLang: SupportedLanguage voiceLang: SupportedLanguage
sentry: boolean
} }
export const defaults: SettingsStoreData = { export const defaults: SettingsStoreData = {
@ -18,6 +19,7 @@ export const defaults: SettingsStoreData = {
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-US', voiceLang: 'en-US',
sentry: true,
} }
const createSequenceStore = () => { const createSequenceStore = () => {