diff --git a/apps/desktop/src/components/pet/floating-pet.tsx b/apps/desktop/src/components/pet/floating-pet.tsx index a5745c00e24..18c8952b846 100644 --- a/apps/desktop/src/components/pet/floating-pet.tsx +++ b/apps/desktop/src/components/pet/floating-pet.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { useGatewayRequest } from '@/app/gateway/hooks/use-gateway-request' import { persistString, storedString } from '@/lib/storage' -import { $petInfo, clearPetUnread, type PetInfo, petProfile, setPetInfo } from '@/store/pet' +import { $petAtRest, $petInfo, $petRoam, $petRoamDir, clearPetUnread, type PetInfo, petProfile, setPetInfo } from '@/store/pet' import { resetPetGallery, setPetScale } from '@/store/pet-gallery' import { $petOverlayActive, initPetOverlayBridge, popOutPet, restorePetOverlay } from '@/store/pet-overlay' import { $activeGatewayProfile, normalizeProfileKey } from '@/store/profile' @@ -11,7 +11,8 @@ import { $gatewayState } from '@/store/session' import { isSecondaryWindow } from '@/store/windows' import { useTheme } from '@/themes/context' -import { PetSprite } from './pet-sprite' +import { PetSprite, roamWalkRow } from './pet-sprite' +import { usePetRoam } from './use-pet-roam' import { type PetZoomAnchor, usePetZoomGesture } from './use-pet-zoom-gesture' // v2: positions are now top/left anchored (v1 stored bottom-anchored values, @@ -104,6 +105,9 @@ export function FloatingPet() { const gatewayState = useStore($gatewayState) const info = useStore($petInfo) const overlayActive = useStore($petOverlayActive) + const roamEnabled = useStore($petRoam) + const atRest = useStore($petAtRest) + const roamDir = useStore($petRoamDir) const [position, setPosition] = useState(loadPosition) const containerRef = useRef(null) @@ -367,6 +371,33 @@ export function FloatingPet() { usePetZoomGesture(containerRef, onScale, active && !overlayActive) + // Commit a roamed-to position back to React state + storage when the wander + // loop settles, so the inline style matches the DOM once the loop stops + // driving it imperatively. Stable identity keeps the roam effect from + // restarting every render. + const commitRoamPosition = useCallback((point: Point) => { + setPosition(point) + persistString(POSITION_KEY, JSON.stringify(point)) + }, []) + + const isDragging = useCallback(() => dragRef.current !== null, []) + + // Roam only the in-window pet, only while it's idle (agent at rest) and not + // popped out into the OS overlay. Activity pauses the wander; the pet reacts + // in place, then resumes strolling when the turn ends. + usePetRoam({ + commit: commitRoamPosition, + containerRef, + enabled: roamEnabled && active && !overlayActive && atRest, + isInteracting: isDragging, + petH, + petW + }) + + // While roaming, drive the directional run row + mirror from the travel + // direction; at rest, fall back to the inward-facing static mascot. + const walk = roamWalkRow(roamDir, info.stateRows) + // While popped out, the desktop overlay window owns the mascot — hide the // in-window one so there aren't two. if (!info.enabled || !info.spritesheetBase64 || overlayActive) { @@ -406,9 +437,14 @@ export function FloatingPet() { />
- +
)