173 lines
5.2 KiB
TypeScript
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);
|
|
}
|