ONEKO FTW!!!!!!
This commit is contained in:
BIN
src/assets/oneko/oneko.gif
Normal file
BIN
src/assets/oneko/oneko.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
@@ -5,6 +5,8 @@
|
|||||||
} from "../../events/cursor";
|
} from "../../events/cursor";
|
||||||
import { appData } from "../../events/app-data";
|
import { appData } from "../../events/app-data";
|
||||||
|
|
||||||
|
import DesktopPet from "./DesktopPet.svelte";
|
||||||
|
|
||||||
function getFriendName(userId: string) {
|
function getFriendName(userId: string) {
|
||||||
const friend = $appData?.friends?.find((f) => f.friend.id === userId);
|
const friend = $appData?.friends?.find((f) => f.friend.id === userId);
|
||||||
return friend ? friend.friend.name : userId.slice(0, 8) + "...";
|
return friend ? friend.friend.name : userId.slice(0, 8) + "...";
|
||||||
@@ -56,13 +58,11 @@
|
|||||||
<div class="absolute inset-0 size-full">
|
<div class="absolute inset-0 size-full">
|
||||||
{#if Object.keys($friendsCursorPositions).length > 0}
|
{#if Object.keys($friendsCursorPositions).length > 0}
|
||||||
{#each Object.entries($friendsCursorPositions) as [userId, position]}
|
{#each Object.entries($friendsCursorPositions) as [userId, position]}
|
||||||
<div
|
<DesktopPet
|
||||||
style:transform="translate({position.raw.x}px, {position.raw.y}px)"
|
targetX={position.raw.x}
|
||||||
class="transition-transform duration-500 flex flex-col gap-1"
|
targetY={position.raw.y}
|
||||||
>
|
name={getFriendName(userId)}
|
||||||
<div class="size-4 rounded-full bg-indigo-500"></div>
|
/>
|
||||||
<span class="text-[0.5rem]">{getFriendName(userId)}</span>
|
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
193
src/routes/scene/DesktopPet.svelte
Normal file
193
src/routes/scene/DesktopPet.svelte
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, onDestroy } from "svelte";
|
||||||
|
import onekoGif from "../../assets/oneko/oneko.gif";
|
||||||
|
|
||||||
|
export let targetX = 0;
|
||||||
|
export let targetY = 0;
|
||||||
|
export let name = "";
|
||||||
|
|
||||||
|
let nekoPosX = 32;
|
||||||
|
let nekoPosY = 32;
|
||||||
|
let frameCount = 0;
|
||||||
|
let idleTime = 0;
|
||||||
|
let currentSprite = { x: -3, y: -3 }; // idle sprite initially
|
||||||
|
|
||||||
|
const nekoSpeed = 10;
|
||||||
|
let animationFrameId: number;
|
||||||
|
let lastFrameTimestamp: number;
|
||||||
|
|
||||||
|
// Sprite constants from oneko.js
|
||||||
|
const spriteSets: Record<string, [number, number][]> = {
|
||||||
|
idle: [[-3, -3]],
|
||||||
|
alert: [[-7, -3]],
|
||||||
|
scratchSelf: [
|
||||||
|
[-5, 0],
|
||||||
|
[-6, 0],
|
||||||
|
[-7, 0],
|
||||||
|
],
|
||||||
|
scratchWallN: [
|
||||||
|
[0, 0],
|
||||||
|
[0, -1],
|
||||||
|
],
|
||||||
|
scratchWallS: [
|
||||||
|
[-7, -1],
|
||||||
|
[-6, -2],
|
||||||
|
],
|
||||||
|
scratchWallE: [
|
||||||
|
[-2, -2],
|
||||||
|
[-2, -3],
|
||||||
|
],
|
||||||
|
scratchWallW: [
|
||||||
|
[-4, 0],
|
||||||
|
[-4, -1],
|
||||||
|
],
|
||||||
|
tired: [[-3, -2]],
|
||||||
|
sleeping: [
|
||||||
|
[-2, 0],
|
||||||
|
[-2, -1],
|
||||||
|
],
|
||||||
|
N: [
|
||||||
|
[-1, -2],
|
||||||
|
[-1, -3],
|
||||||
|
],
|
||||||
|
NE: [
|
||||||
|
[0, -2],
|
||||||
|
[0, -3],
|
||||||
|
],
|
||||||
|
E: [
|
||||||
|
[-3, 0],
|
||||||
|
[-3, -1],
|
||||||
|
],
|
||||||
|
SE: [
|
||||||
|
[-5, -1],
|
||||||
|
[-5, -2],
|
||||||
|
],
|
||||||
|
S: [
|
||||||
|
[-6, -3],
|
||||||
|
[-7, -2],
|
||||||
|
],
|
||||||
|
SW: [
|
||||||
|
[-5, -3],
|
||||||
|
[-6, -1],
|
||||||
|
],
|
||||||
|
W: [
|
||||||
|
[-4, -2],
|
||||||
|
[-4, -3],
|
||||||
|
],
|
||||||
|
NW: [
|
||||||
|
[-1, 0],
|
||||||
|
[-1, -1],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function setSprite(name: string, frame: number) {
|
||||||
|
const sprites = spriteSets[name];
|
||||||
|
const sprite = sprites[frame % sprites.length];
|
||||||
|
currentSprite = { x: sprite[0] * 32, y: sprite[1] * 32 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function frame(timestamp: number) {
|
||||||
|
if (!lastFrameTimestamp) {
|
||||||
|
lastFrameTimestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 100ms per frame for the animation loop
|
||||||
|
if (timestamp - lastFrameTimestamp > 100) {
|
||||||
|
lastFrameTimestamp = timestamp;
|
||||||
|
updatePosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
animationFrameId = requestAnimationFrame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePosition() {
|
||||||
|
frameCount += 1;
|
||||||
|
const diffX = nekoPosX - targetX;
|
||||||
|
const diffY = nekoPosY - targetY;
|
||||||
|
const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
|
||||||
|
|
||||||
|
// If close enough, stop moving and idle
|
||||||
|
if (distance < nekoSpeed || distance < 48) {
|
||||||
|
setSprite("idle", 0);
|
||||||
|
idleTime += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alert behavior: pause briefly before moving if we were idling
|
||||||
|
if (idleTime > 1) {
|
||||||
|
setSprite("alert", 0);
|
||||||
|
idleTime = Math.min(idleTime, 7);
|
||||||
|
idleTime -= 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
idleTime = 0;
|
||||||
|
|
||||||
|
// Calculate direction
|
||||||
|
let direction = "";
|
||||||
|
direction = diffY / distance > 0.5 ? "N" : "";
|
||||||
|
direction += diffY / distance < -0.5 ? "S" : "";
|
||||||
|
direction += diffX / distance > 0.5 ? "W" : "";
|
||||||
|
direction += diffX / distance < -0.5 ? "E" : "";
|
||||||
|
|
||||||
|
// Fallback if direction is empty (shouldn't happen with logic above but good safety)
|
||||||
|
if (direction === "") direction = "idle";
|
||||||
|
|
||||||
|
setSprite(direction, frameCount);
|
||||||
|
|
||||||
|
// Move towards target
|
||||||
|
nekoPosX -= (diffX / distance) * nekoSpeed;
|
||||||
|
nekoPosY -= (diffY / distance) * nekoSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// Initialize position to target so it doesn't fly in from 32,32 every time
|
||||||
|
nekoPosX = targetX;
|
||||||
|
nekoPosY = targetY;
|
||||||
|
|
||||||
|
animationFrameId = requestAnimationFrame(frame);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (animationFrameId) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="desktop-pet flex flex-col items-center"
|
||||||
|
style="
|
||||||
|
transform: translate({nekoPosX - 16}px, {nekoPosY - 16}px);
|
||||||
|
z-index: 50;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="pixelated"
|
||||||
|
style="
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background-image: url({onekoGif});
|
||||||
|
background-position: {currentSprite.x}px {currentSprite.y}px;
|
||||||
|
"
|
||||||
|
></div>
|
||||||
|
<span
|
||||||
|
class="text-[10px] bg-black/50 text-white px-1 rounded backdrop-blur-sm mt-1 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.desktop-pet {
|
||||||
|
position: fixed; /* Fixed relative to the viewport/container */
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
pointer-events: none; /* Let clicks pass through */
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pixelated {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user