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`; +}