Files
website/src/lib/pet/behavior.ts
T

173 lines
5.2 KiB
TypeScript

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<PetActivity, SecondsRange> = {
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);
}