diff --git a/src/lib/components/PetSprite.svelte b/src/lib/components/PetSprite.svelte new file mode 100644 index 0000000..18a82b8 --- /dev/null +++ b/src/lib/components/PetSprite.svelte @@ -0,0 +1,16 @@ + + +
diff --git a/src/lib/composables/usePetState.ts b/src/lib/composables/usePetState.ts new file mode 100644 index 0000000..5b91bd9 --- /dev/null +++ b/src/lib/composables/usePetState.ts @@ -0,0 +1,148 @@ +import { writable, get } from "svelte/store"; +import { SPRITE_SETS, SPRITE_SIZE, PET_SPEED } from "../constants/pet-sprites"; + +export function usePetState(initialX = 32, initialY = 32) { + const position = writable({ x: initialX, y: initialY }); + const currentSprite = writable({ x: -3, y: -3 }); + + // Internal state not exposed directly + let frameCount = 0; + let idleTime = 0; + let idleAnimation: string | null = null; + let idleAnimationFrame = 0; + + function setSprite(name: string, frame: number) { + const sprites = SPRITE_SETS[name]; + if (!sprites) return; + const sprite = sprites[frame % sprites.length]; + currentSprite.set({ + x: sprite[0] * SPRITE_SIZE, + y: sprite[1] * SPRITE_SIZE, + }); + } + + function resetIdleAnimation() { + idleAnimation = null; + idleAnimationFrame = 0; + } + + function handleIdle(windowWidth: number, windowHeight: number) { + idleTime += 1; + const currentPos = get(position); + + // every ~ 20 seconds (idleTime increments every frame, with ~10 frames/second, so ~200 frames) + if ( + idleTime > 10 && + Math.floor(Math.random() * 200) == 0 && + idleAnimation == null + ) { + let availableIdleAnimations = ["sleeping", "scratchSelf"]; + if (currentPos.x < 32) { + availableIdleAnimations.push("scratchWallW"); + } + if (currentPos.y < 32) { + availableIdleAnimations.push("scratchWallN"); + } + if (currentPos.x > windowWidth - 32) { + availableIdleAnimations.push("scratchWallE"); + } + if (currentPos.y > windowHeight - 32) { + availableIdleAnimations.push("scratchWallS"); + } + idleAnimation = + availableIdleAnimations[ + Math.floor(Math.random() * availableIdleAnimations.length) + ]; + } + + if (!idleAnimation) { + setSprite("idle", 0); + return; + } + + switch (idleAnimation) { + case "sleeping": + if (idleAnimationFrame < 8) { + setSprite("tired", 0); + break; + } + setSprite("sleeping", Math.floor(idleAnimationFrame / 4)); + if (idleAnimationFrame > 192) { + resetIdleAnimation(); + } + break; + case "scratchWallN": + case "scratchWallS": + case "scratchWallE": + case "scratchWallW": + case "scratchSelf": + setSprite(idleAnimation, idleAnimationFrame); + if (idleAnimationFrame > 9) { + resetIdleAnimation(); + } + break; + default: + setSprite("idle", 0); + return; + } + idleAnimationFrame += 1; + } + + function updatePosition( + targetX: number, + targetY: number, + windowWidth: number, + windowHeight: number, + ) { + frameCount += 1; + const currentPos = get(position); + + const diffX = currentPos.x - targetX; + const diffY = currentPos.y - targetY; + const distance = Math.sqrt(diffX ** 2 + diffY ** 2); + + // If close enough, stop moving and idle + if (distance < PET_SPEED || distance < 48) { + handleIdle(windowWidth, windowHeight); + 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; + idleAnimation = null; + idleAnimationFrame = 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 + if (direction === "") direction = "idle"; + + setSprite(direction, frameCount); + + // Move towards target + position.update((p) => ({ + x: p.x - (diffX / distance) * PET_SPEED, + y: p.y - (diffY / distance) * PET_SPEED, + })); + } + + return { + position, + currentSprite, + updatePosition, + // Helper to force position if needed (e.g. on mount) + setPosition: (x: number, y: number) => position.set({ x, y }), + }; +} diff --git a/src/lib/constants/pet-sprites.ts b/src/lib/constants/pet-sprites.ts new file mode 100644 index 0000000..2c751fb --- /dev/null +++ b/src/lib/constants/pet-sprites.ts @@ -0,0 +1,69 @@ +export type SpriteCoordinates = [number, number]; +export type SpriteSet = Record; + +export const SPRITE_SETS: SpriteSet = { + 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 const SPRITE_SIZE = 32; +export const PET_SPEED = 10; +export const ANIMATION_FRAME_RATE = 100; // ms diff --git a/src/lib/utils/sprite-utils.ts b/src/lib/utils/sprite-utils.ts new file mode 100644 index 0000000..21b055d --- /dev/null +++ b/src/lib/utils/sprite-utils.ts @@ -0,0 +1,28 @@ +import { invoke } from "@tauri-apps/api/core"; +import onekoGif from "../../assets/oneko/oneko.gif"; + +export interface RecolorOptions { + bodyColor: string; + outlineColor: string; + applyTexture?: boolean; +} + +export async function getSpriteSheetUrl( + options?: RecolorOptions, +): Promise { + if (!options || !options.bodyColor || !options.outlineColor) { + return onekoGif; + } + + try { + const result = await invoke("recolor_gif_base64", { + whiteColorHex: options.bodyColor, + blackColorHex: options.outlineColor, + applyTexture: options.applyTexture ?? true, + }); + return `data:image/gif;base64,${result}`; + } catch (e) { + console.error("Failed to recolor sprite:", e); + return onekoGif; + } +} diff --git a/src/routes/app-menu/components/doll-preview.svelte b/src/routes/app-menu/components/doll-preview.svelte index 0569124..43e8a20 100644 --- a/src/routes/app-menu/components/doll-preview.svelte +++ b/src/routes/app-menu/components/doll-preview.svelte @@ -1,6 +1,8 @@