data endpoint for entity 90008 (aka. a website)

feat: add desktop pet thing

ptr.pet ff390edd 39011085

verified
Changed files
+146 -1
src
components
routes
static
+139
src/components/pet.svelte
···
+
<script lang="ts">
+
import { draggable } from '@neodrag/svelte';
+
+
let position = $state({ x: 0, y: 0 });
+
let rotation = $state(0.0);
+
let sprite = $state('/pet/idle.webp');
+
let flip = $state(false);
+
let dragged = $state(false);
+
+
let targetX = 120;
+
let speed = 10.0;
+
let tickRate = 20;
+
let delta = 1.0 / tickRate;
+
+
let strideRadius = 4.0;
+
let strideAngle = 0;
+
+
const turnStrideWheel = (by: number) => {
+
strideAngle += by / strideRadius;
+
if (strideAngle > Math.PI * 2) {
+
strideAngle -= Math.PI * 2;
+
} else if (strideAngle < 0) {
+
strideAngle += Math.PI * 2;
+
}
+
};
+
+
let targetRotation = $state(0.0);
+
let rotationVelocity = $state(0.0);
+
let springStiffness = 20.0; // How quickly rotation returns to target
+
let springDamping = 0.2; // Damping factor to prevent oscillation
+
+
const updateRotationSpring = () => {
+
// Spring physics: calculate force based on distance from target
+
const springForce = (targetRotation - rotation) * springStiffness;
+
+
// Apply damping to velocity
+
rotationVelocity = rotationVelocity * (1 - springDamping) + springForce * delta;
+
+
// Update rotation based on velocity
+
rotation += rotationVelocity * delta;
+
+
// If we're very close to target and barely moving, just snap to target
+
if (Math.abs(rotation - targetRotation) < 0.01 && Math.abs(rotationVelocity) < 0.01) {
+
rotation = targetRotation;
+
rotationVelocity = 0;
+
}
+
};
+
+
// Add spring update to the move function
+
setInterval(updateRotationSpring, tickRate);
+
+
const lerp = (from: number, to: number, weight: number) => {
+
return from + (to - from) * weight;
+
};
+
+
const moveTowards = (from: number, to: number, by: number) => {
+
let d = (to - from) * 1.0;
+
let l = Math.abs(d);
+
let s = Math.sign(d);
+
let moveBy = s * Math.min(l, by) * delta;
+
return moveBy;
+
};
+
+
const move = () => {
+
if (dragged) {
+
return;
+
}
+
+
if (position.y !== 0) {
+
position.y = Math.ceil(lerp(position.y, 0.0, 0.2));
+
return;
+
}
+
+
let moveByX = moveTowards(position.x, targetX, speed);
+
position.x += moveByX;
+
+
turnStrideWheel(moveByX);
+
+
flip = moveByX < 0.0;
+
if (moveByX > 0.1 || moveByX < -0.1) {
+
sprite = strideAngle % Math.PI < Math.PI * 0.5 ? '/pet/walk1.webp' : '/pet/walk2.webp';
+
} else {
+
sprite = '/pet/idle.webp';
+
}
+
};
+
+
setInterval(move, tickRate);
+
+
const pickNewTargetX = () => {
+
const viewportWidth = self.innerWidth || null;
+
if (viewportWidth !== null && Math.abs(position.x - targetX) < 5) {
+
targetX = Math.max(
+
Math.min(targetX + (Math.random() - 0.5) * 500.0, viewportWidth * 0.9),
+
viewportWidth * 0.1
+
);
+
}
+
// Set a random interval for the next target update (between 4-10 seconds)
+
const randomDelay = Math.floor(Math.random() * 6000) + 4000;
+
setTimeout(pickNewTargetX, randomDelay);
+
};
+
+
// Start the process
+
setTimeout(pickNewTargetX, 1000);
+
</script>
+
+
<!-- svelte-ignore a11y_missing_attribute -->
+
<div
+
use:draggable={{
+
position,
+
applyUserSelectHack: true,
+
handle: 'img',
+
bounds: {
+
bottom: (window.innerHeight / 100) * 5.5
+
},
+
onDragStart: () => {
+
sprite = '/pet/pick.webp';
+
dragged = true;
+
},
+
onDrag: ({ offsetX, offsetY, event }) => {
+
position.x = offsetX;
+
position.y = offsetY;
+
rotationVelocity += event.movementY * delta + event.movementX * delta;
+
},
+
onDragEnd: () => {
+
dragged = false;
+
}
+
}}
+
class="absolute bottom-[5vh] z-[1000]"
+
>
+
<img
+
draggable="false"
+
class="invert"
+
style="
+
image-rendering: pixelated !important;
+
transform: rotate({rotation}rad) scaleX({flip ? -1 : 1});
+
"
+
src={sprite}
+
/>
+
</div>
+7 -1
src/routes/+layout.svelte
···
<script lang="ts">
import getTitle from '$lib/getTitle';
+
import { isMobile } from '$lib/window';
import NavButton from '../components/navButton.svelte';
+
import Pet from '../components/pet.svelte';
import Tooltip from '../components/tooltip.svelte';
import '../styles/app.css';
···
{@render children?.()}
</div>
-
<nav class="w-full min-h-[5vh] max-h-[6vh] fixed bottom-0 z-[999] bg-ralsei-black overflow-visible">
+
{#if !isMobile()}
+
<Pet></Pet>
+
{/if}
+
+
<nav class="w-full min-h-[5vh] max-h-[5vh] fixed bottom-0 z-[999] bg-ralsei-black overflow-visible">
<div
class="
max-w-full max-h-fit p-1 z-[999]
static/pet/idle.webp

This is a binary file and will not be displayed.

static/pet/pick.webp

This is a binary file and will not be displayed.

static/pet/walk1.webp

This is a binary file and will not be displayed.

static/pet/walk2.webp

This is a binary file and will not be displayed.