import { findPath, nearestFreePoint } from './pathfinding'; import { clampToWorld, furthestX, furthestY, isSpotBlocked, petHeight, petWidth, type PetWorld, type Point } from './world'; export type PetAction = 'idle' | 'walk' | 'sleep' | 'hop' | 'held' | 'activity'; export type PetActivity = 'wave' | 'scan' | 'dance' | 'spin' | 'charge' | 'compute'; export type PetExpression = 'awake' | 'sleeping' | 'happy'; export interface Pet { x: number; y: number; facing: 1 | -1; action: PetAction; activity: PetActivity | null; actionTimeLeft: number; path: Point[]; commanded: boolean; } interface SecondsRange { minimum: number; maximum: number; } const wanderSpeed = 45; const commandedSpeed = 140; const hopSeconds = 0.4; const idleSeconds = { minimum: 2, maximum: 5 }; const sleepSeconds = { minimum: 8, maximum: 16 }; const activitySeconds: Record = { wave: { minimum: 2, maximum: 3 }, scan: { minimum: 3, maximum: 5 }, dance: { minimum: 4, maximum: 7 }, spin: { minimum: 1.6, maximum: 1.6 }, charge: { minimum: 4, maximum: 6 }, compute: { minimum: 3, maximum: 6 } }; const activities = Object.keys(activitySeconds) as PetActivity[]; const happyActivities: PetActivity[] = ['dance', 'spin']; const nextActionChances = { walk: 0.5, activity: 0.3, idle: 0.1 }; function randomBetween(minimum: number, maximum: number): number { return minimum + Math.random() * (maximum - minimum); } function setAction(pet: Pet, action: PetAction, seconds: SecondsRange | number = 0) { pet.action = action; pet.activity = null; pet.actionTimeLeft = typeof seconds === 'number' ? seconds : randomBetween(seconds.minimum, seconds.maximum); } function startActivity(pet: Pet) { const activity = activities[Math.floor(Math.random() * activities.length)]; setAction(pet, 'activity', activitySeconds[activity]); pet.activity = activity; } function startWalk(pet: Pet, goal: Point, world: PetWorld, commanded: boolean) { const isStuck = isSpotBlocked(pet, world); const origin = isStuck ? nearestFreePoint(pet, world) : { x: pet.x, y: pet.y }; const destination = nearestFreePoint(goal, world); const path = findPath(origin, destination, world); if (isStuck) path.unshift(origin); if (path.length === 0) { setAction(pet, 'idle', idleSeconds); return; } setAction(pet, 'walk'); pet.path = path; pet.commanded = commanded; } function chooseNextAction(pet: Pet, world: PetWorld) { const roll = Math.random(); const randomSpot = { x: randomBetween(0, furthestX(world)), y: randomBetween(0, furthestY(world)) }; if (roll < nextActionChances.walk) startWalk(pet, randomSpot, world, false); else if (roll < nextActionChances.walk + nextActionChances.activity) startActivity(pet); else if (roll < nextActionChances.walk + nextActionChances.activity + nextActionChances.idle) setAction(pet, 'idle', idleSeconds); else setAction(pet, 'sleep', sleepSeconds); } export function createPet(world: PetWorld): Pet { const spawn = nearestFreePoint({ x: furthestX(world) / 2, y: furthestY(world) / 2 }, world); return { ...spawn, facing: 1, action: 'idle', activity: null, actionTimeLeft: 1, path: [], commanded: false }; } export function petExpression(pet: Pet): PetExpression { if (pet.action === 'sleep') return 'sleeping'; if (pet.action === 'hop' || pet.action === 'held') return 'happy'; if (pet.activity && happyActivities.includes(pet.activity)) return 'happy'; return 'awake'; } export function commandPet(pet: Pet, pointX: number, pointY: number, world: PetWorld) { if (pet.action === 'held') return; startWalk(pet, { x: pointX - petWidth / 2, y: pointY - petHeight }, world, true); } export function hopPet(pet: Pet) { if (pet.action === 'held') return; setAction(pet, 'hop', hopSeconds); } export function holdPet(pet: Pet) { setAction(pet, 'held'); pet.path = []; } export function dropPet(pet: Pet) { setAction(pet, 'hop', hopSeconds); } export function placePet(pet: Pet, x: number, y: number, world: PetWorld) { const spot = clampToWorld({ x, y }, world); pet.x = spot.x; pet.y = spot.y; } function followPath(pet: Pet, seconds: number) { const waypoint = pet.path[0]; if (!waypoint) { setAction(pet, 'idle', idleSeconds); return; } const distanceX = waypoint.x - pet.x; const distanceY = waypoint.y - pet.y; const remainingDistance = Math.hypot(distanceX, distanceY); const stepDistance = (pet.commanded ? commandedSpeed : wanderSpeed) * seconds; if (Math.abs(distanceX) > 1) pet.facing = distanceX > 0 ? 1 : -1; if (remainingDistance <= stepDistance) { pet.x = waypoint.x; pet.y = waypoint.y; pet.path.shift(); if (pet.path.length === 0) setAction(pet, 'idle', idleSeconds); return; } pet.x += (distanceX / remainingDistance) * stepDistance; pet.y += (distanceY / remainingDistance) * stepDistance; } export function stepPet(pet: Pet, seconds: number, world: PetWorld) { placePet(pet, pet.x, pet.y, world); if (pet.action === 'held') return; if (pet.action === 'walk') { followPath(pet, seconds); return; } pet.actionTimeLeft -= seconds; if (pet.actionTimeLeft > 0) return; if (isSpotBlocked(pet, world)) startWalk(pet, pet, world, false); else if (pet.action === 'idle') chooseNextAction(pet, world); else setAction(pet, 'idle', idleSeconds); }