diff --git a/src/routes/scene/+page.svelte b/src/routes/scene/+page.svelte
index a812a75..a73baf6 100644
--- a/src/routes/scene/+page.svelte
+++ b/src/routes/scene/+page.svelte
@@ -8,7 +8,17 @@
currentPresenceState,
} from "../../events/user-status";
import { commands } from "$lib/bindings";
+ import { getSpriteSheetUrl } from "$lib/utils/sprite-utils";
import DebugBar from "./components/debug-bar.svelte";
+ import Neko from "./components/neko/neko.svelte";
+
+ let spriteUrl = $state("");
+
+ $effect(() => {
+ getSpriteSheetUrl().then((url) => {
+ spriteUrl = url;
+ });
+ });
@@ -19,6 +29,11 @@
await commands.setSceneInteractive(false, true);
}}>
+
= nekoSpeed && distance >= 48) {
+ return { isIdle: false, idleAnimation, idleAnimationFrame, idleTime };
+ }
+
+ let newIdleTime = idleTime + 1;
+ let newIdleAnimation = idleAnimation;
+ let newIdleFrame = idleAnimationFrame;
+
+ if (
+ newIdleTime > 10 &&
+ Math.floor(Math.random() * 200) == 0 &&
+ newIdleAnimation == null
+ ) {
+ let availableIdleAnimations = ["sleeping", "scratchSelf"];
+ if (nekoPos.x < 32) {
+ availableIdleAnimations.push("scratchWallW");
+ }
+ if (nekoPos.y < 32) {
+ availableIdleAnimations.push("scratchWallN");
+ }
+ if (nekoPos.x > window.innerWidth - 32) {
+ availableIdleAnimations.push("scratchWallE");
+ }
+ if (nekoPos.y > window.innerHeight - 32) {
+ availableIdleAnimations.push("scratchWallS");
+ }
+ newIdleAnimation =
+ availableIdleAnimations[
+ Math.floor(Math.random() * availableIdleAnimations.length)
+ ];
+ }
+
+ switch (newIdleAnimation) {
+ case "sleeping":
+ if (newIdleFrame < 8) {
+ setSprite(nekoEl, "tired", 0);
+ } else {
+ setSprite(nekoEl, "sleeping", Math.floor(newIdleFrame / 4));
+ }
+ if (newIdleFrame > 192) {
+ newIdleAnimation = null;
+ newIdleFrame = 0;
+ }
+ break;
+ case "scratchWallN":
+ case "scratchWallS":
+ case "scratchWallE":
+ case "scratchWallW":
+ case "scratchSelf":
+ setSprite(nekoEl, newIdleAnimation, newIdleFrame);
+ if (newIdleFrame > 9) {
+ newIdleAnimation = null;
+ newIdleFrame = 0;
+ }
+ break;
+ default:
+ setSprite(nekoEl, "idle", 0);
+ return { isIdle: true, idleAnimation: null, idleAnimationFrame: 0, idleTime: newIdleTime };
+ }
+
+ return {
+ isIdle: true,
+ idleAnimation: newIdleAnimation,
+ idleAnimationFrame: newIdleFrame + 1,
+ idleTime: newIdleTime,
+ };
+}
diff --git a/src/routes/scene/components/neko/neko.svelte b/src/routes/scene/components/neko/neko.svelte
new file mode 100644
index 0000000..56e5e7f
--- /dev/null
+++ b/src/routes/scene/components/neko/neko.svelte
@@ -0,0 +1,104 @@
+
+
+
diff --git a/src/routes/scene/components/neko/physics.ts b/src/routes/scene/components/neko/physics.ts
new file mode 100644
index 0000000..35525d3
--- /dev/null
+++ b/src/routes/scene/components/neko/physics.ts
@@ -0,0 +1,52 @@
+import { nekoSpeed } from "./sprites";
+
+export interface Position {
+ x: number;
+ y: number;
+}
+
+export function calculateDirection(
+ fromX: number,
+ fromY: number,
+ toX: number,
+ toY: number,
+): { direction: string; distance: number } {
+ const diffX = fromX - toX;
+ const diffY = fromY - toY;
+ const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
+
+ 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" : "";
+
+ return { direction, distance };
+}
+
+export function moveTowards(
+ currentX: number,
+ currentY: number,
+ targetX: number,
+ targetY: number,
+ speed: number = nekoSpeed,
+): Position {
+ const diffX = targetX - currentX;
+ const diffY = targetY - currentY;
+ const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
+
+ if (distance === 0) return { x: currentX, y: currentY };
+
+ const newX = currentX + (diffX / distance) * speed;
+ const newY = currentY + (diffY / distance) * speed;
+
+ return clampPosition(newX, newY);
+}
+
+export function clampPosition(x: number, y: number): Position {
+ const margin = 16;
+ return {
+ x: Math.min(Math.max(margin, x), window.innerWidth - margin),
+ y: Math.min(Math.max(margin, y), window.innerHeight - margin),
+ };
+}
diff --git a/src/routes/scene/components/neko/sprites.ts b/src/routes/scene/components/neko/sprites.ts
new file mode 100644
index 0000000..6045f79
--- /dev/null
+++ b/src/routes/scene/components/neko/sprites.ts
@@ -0,0 +1,76 @@
+export const SPRITE_SIZE = 32;
+export const nekoSpeed = 10;
+
+export const spriteSets: Record = {
+ 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],
+ ],
+};
+
+export function setSprite(
+ el: HTMLDivElement,
+ name: string,
+ frame: number,
+): void {
+ const sprites = spriteSets[name];
+ if (!sprites) return;
+ const sprite = sprites[frame % sprites.length];
+ el.style.backgroundPosition = `${sprite[0] * SPRITE_SIZE}px ${sprite[1] * SPRITE_SIZE}px`;
+}