data endpoint for entity 90008 (aka. a website)
at svelte 8.7 kB view raw
1<script module lang="ts"> 2 import { get, writable } from 'svelte/store'; 3 4 export const localDistanceTravelled = writable(0.0); 5 export const localBounces = writable(0); 6</script> 7 8<script lang="ts"> 9 import { draggable } from '@neodrag/svelte'; 10 import { browser } from '$app/environment'; 11 12 interface Props { 13 apiToken: string; 14 } 15 16 let { apiToken }: Props = $props(); 17 18 let lastDragged = 0; 19 let mouseX = 0; 20 let mouseY = 0; 21 22 let position = $state({ x: 0, y: 0 }); 23 let rotation = $state(0.0); 24 let sprite = $state('/pet/idle.webp'); 25 let flip = $state(false); 26 let dragged = $state(false); 27 28 let targetX = 120; 29 let speed = 10.0; 30 let tickRate = 20; 31 let delta = 1.0 / tickRate; 32 33 let strideRadius = 4.0; 34 let strideAngle = 0; 35 36 const turnStrideWheel = (by: number) => { 37 strideAngle += by / strideRadius; 38 if (strideAngle > Math.PI * 2) { 39 strideAngle -= Math.PI * 2; 40 } else if (strideAngle < 0) { 41 strideAngle += Math.PI * 2; 42 } 43 }; 44 45 let targetRotation = 0.0; 46 let rotationVelocity = 0.0; 47 let springStiffness = 20.0; // How quickly rotation returns to target 48 let springDamping = 0.2; // Damping factor to prevent oscillation 49 50 const updateRotationSpring = () => { 51 // Spring physics: calculate force based on distance from target 52 const springForce = (targetRotation - rotation) * springStiffness; 53 54 // Apply damping to velocity 55 rotationVelocity = rotationVelocity * (1 - springDamping) + springForce * delta; 56 57 // Update rotation based on velocity 58 rotation += rotationVelocity * delta; 59 60 // If we're very close to target and barely moving, just snap to target 61 if (Math.abs(rotation - targetRotation) < 0.01 && Math.abs(rotationVelocity) < 0.01) { 62 rotation = targetRotation; 63 rotationVelocity = 0; 64 } 65 }; 66 67 // Add spring update to the move function 68 if (browser) setInterval(updateRotationSpring, tickRate); 69 70 const moveTowards = (from: number, to: number, by: number) => { 71 let d = (to - from) * 1.0; 72 let l = Math.abs(d); 73 let s = Math.sign(d); 74 let moveBy = s * Math.min(l, by) * delta; 75 return moveBy; 76 }; 77 78 // Physics constants 79 let velocityX = 0; 80 let velocityY = 0; 81 let gravity = 200.0; // Gravity strength (positive because -Y is up) 82 let friction = 0.96; // Air friction 83 let groundFriction = 0.9; // Ground friction 84 let bounciness = 0.8; // How much energy is preserved on bounce 85 86 const sendBounceMetrics = () => { 87 fetch(`/_api/pet/bounce?_token=${apiToken}`); 88 localBounces.set(get(localBounces) + 1); 89 }; 90 91 let deltaTravelled = 0.0; 92 let deltaTravelledTotal = 0.0; 93 const updateDistanceTravelled = () => { 94 if (deltaTravelled > 0.1 || deltaTravelled < -0.1) { 95 localDistanceTravelled.update((n) => { 96 n += deltaTravelled; 97 return n; 98 }); 99 deltaTravelledTotal += deltaTravelled; 100 } 101 deltaTravelled = 0.0; 102 }; 103 104 const sendTotalDistance = () => { 105 fetch(`/_api/pet/distance?_token=${apiToken}`, { 106 method: 'POST', 107 body: deltaTravelledTotal.toString() 108 }); 109 deltaTravelledTotal = 0.0; 110 }; 111 112 // sending every 5 seconds is probably reliable enough 113 if (browser) setInterval(sendTotalDistance, 1000 * 5); 114 115 const move = () => { 116 if (dragged) return; 117 118 // Apply physics when pet is in motion 119 if (velocityX !== 0 || velocityY !== 0 || position.y !== 0) { 120 // Apply gravity (remember negative Y is upward) 121 velocityY += gravity * delta; 122 123 // Apply friction 124 const fric = position.y === 0 ? groundFriction : friction; 125 velocityX *= fric; 126 velocityY *= fric; 127 128 // Update position 129 const moveX = velocityX * delta; 130 const moveY = velocityY * delta; 131 position.x += moveX; 132 position.y += moveY; 133 134 deltaTravelled += Math.sqrt(moveX ** 2 + moveY ** 2); 135 updateDistanceTravelled(); 136 137 // Handle window boundaries 138 const viewportWidth = window.innerWidth; 139 140 // Bounce off sides 141 if (position.x < 0) { 142 position.x = 0; 143 velocityX = -velocityX * bounciness; 144 sendBounceMetrics(); 145 } else if (position.x > viewportWidth) { 146 position.x = viewportWidth; 147 velocityX = -velocityX * bounciness; 148 sendBounceMetrics(); 149 } 150 151 // Bounce off bottom (floor) 152 if (position.y > 0) { 153 position.y = 0; 154 velocityY = -velocityY * bounciness; 155 // Only bounce if velocity is significant 156 if (Math.abs(velocityY) < 80) { 157 velocityY = 0; 158 position.y = 0; 159 } else { 160 sendBounceMetrics(); 161 } 162 } 163 164 // reset velocity 165 if (Math.abs(velocityX) < 5 && Math.abs(velocityY) < 5) { 166 velocityX = 0; 167 velocityY = 0; 168 } 169 170 // Update flip based on velocity 171 if (Math.abs(velocityX) > 0.5) { 172 flip = velocityX < 0; 173 } 174 175 targetRotation = velocityX * 0.02 + velocityY * 0.01; 176 177 return; 178 } 179 180 // Normal movement when not physics-based 181 let moveByX = moveTowards(position.x, targetX, speed * ((self.innerWidth ?? 1600.0) / 1600.0)); 182 position.x += moveByX; 183 184 turnStrideWheel(moveByX); 185 186 flip = moveByX < 0.0; 187 if (moveByX > 0.1 || moveByX < -0.1) { 188 sprite = strideAngle % Math.PI < Math.PI * 0.5 ? '/pet/walk1.webp' : '/pet/walk2.webp'; 189 } else { 190 sprite = '/pet/idle.webp'; 191 } 192 193 deltaTravelled += Math.abs(moveByX); 194 updateDistanceTravelled(); 195 }; 196 197 if (browser) setInterval(move, tickRate); 198 199 const shake = (event: DeviceMotionEvent) => { 200 const accel = event.acceleration ?? event.accelerationIncludingGravity; 201 if (accel === null || accel.x === null || accel.y === null) return; 202 if (Math.abs(accel.x) + Math.abs(accel.y) < 40.0) return; 203 // make it so that it amplifies motion proportionally to the window size 204 const windowRatio = (window.innerWidth * 1.0) / (window.innerHeight * 1.0); 205 velocityX += accel.x * windowRatio * 5.0; 206 velocityY += accel.y * (1.0 / windowRatio) * 5.0; 207 sprite = '/pet/pick.webp'; 208 }; 209 210 if (browser) self.ondevicemotion = shake; 211 212 // this is for ios 213 const askForShakePermission = () => { 214 if ( 215 typeof DeviceMotionEvent !== 'undefined' && 216 // eslint-disable-next-line @typescript-eslint/no-explicit-any 217 typeof (DeviceMotionEvent as any).requestPermission === 'function' 218 ) { 219 // eslint-disable-next-line @typescript-eslint/no-explicit-any 220 (DeviceMotionEvent as any) 221 .requestPermission() 222 .then((permissionState: string) => { 223 if (permissionState === 'granted') { 224 self.ondevicemotion = shake; 225 } 226 }) 227 .catch(console.error); 228 } 229 }; 230 231 const pickNewTargetX = () => { 232 const viewportWidth = self.innerWidth || null; 233 if (viewportWidth !== null && Math.abs(position.x - targetX) < 5) { 234 targetX = Math.max( 235 Math.min(targetX + (Math.random() - 0.5) * (viewportWidth * 0.5), viewportWidth * 0.9), 236 viewportWidth * 0.1 237 ); 238 } 239 // Set a random interval for the next target update (between 4-10 seconds) 240 const randomDelay = Math.floor(Math.random() * 6000) + 4000; 241 setTimeout(pickNewTargetX, randomDelay); 242 }; 243 244 // Start the process 245 if (browser) setTimeout(pickNewTargetX, 1000); 246</script> 247 248<!-- svelte-ignore a11y_missing_attribute --> 249<div 250 use:draggable={{ 251 position, 252 applyUserSelectHack: true, 253 handle: 'img', 254 bounds: { 255 bottom: (window.innerHeight / 100) * 5.5 256 }, 257 onDragStart: () => { 258 sprite = '/pet/pick.webp'; 259 dragged = true; 260 }, 261 onDrag: ({ offsetX, offsetY, event }) => { 262 position.x = offsetX; 263 position.y = offsetY; 264 const mouseXD = event.movementX * delta; 265 const mouseYD = event.movementY * delta; 266 deltaTravelled += Math.sqrt(mouseXD ** 2 + mouseYD ** 2); 267 // reset mouse movement if it's not moving in the same direction so it doesnt accumulate its weird!@!@ 268 mouseX = Math.sign(mouseXD) != Math.sign(mouseX) ? mouseXD : mouseX + mouseXD; 269 mouseY = Math.sign(mouseYD) != Math.sign(mouseY) ? mouseYD : mouseY + mouseYD; 270 rotationVelocity += mouseXD + mouseYD; 271 lastDragged = Date.now(); 272 }, 273 onDragEnd: () => { 274 // reset mouse movement if we stopped for longer than some time 275 if (Date.now() - lastDragged > 50) { 276 mouseX = 0.0; 277 mouseY = 0.0; 278 } 279 // apply velocity based on rotation since we already keep track of that 280 velocityX = mouseX * 70.0; 281 velocityY = mouseY * 50.0; 282 updateDistanceTravelled(); 283 // reset mouse movement we dont want it to accumulate 284 mouseX = 0.0; 285 mouseY = 0.0; 286 dragged = false; 287 } 288 }} 289 class="fixed bottom-[5vh] z-[1000] hover:animate-squiggle" 290 style="cursor: url('/icons/gaze.webp'), pointer;" 291> 292 <!-- svelte-ignore a11y_no_noninteractive_element_interactions --> 293 <!-- svelte-ignore a11y_click_events_have_key_events --> 294 <img 295 draggable="false" 296 onclick={askForShakePermission} 297 style=" 298 image-rendering: pixelated !important; 299 transform: rotate({rotation}rad) scaleX({flip ? -1 : 1}); 300 filter: invert(100%) drop-shadow(2px 2px 0 black) drop-shadow(-2px -2px 0 black); 301 " 302 src={sprite} 303 /> 304</div>