FERRETGame engine over Babylon.js
Ferret

@ferret/interaction

Interactables with focus and prompts, inventory and stock props: doors, level doors, pickups, levers.

22 exports · source in packages/interaction/src

Classes

Container class

class Container extends Component<ContainerParams>

Somewhere to keep items (a locker, a chest): the counterpart of Inventory. If the entity has a Door the container works only while it is open. put(actor, item) moves an item from the actor's Inventory; take(actor, item) moves it to the actor's Inventory. Both return false and change nothing when the door is shut, the actor lacks the item, the container is full or does not hold it. Events: container:put / container:take { container, actor, item, count }.

schema: { readonly capacity: { readonly type: "number"; readonly min: 0; }; readonly items: { readonly type: "object"; }; readonly alwaysOpen: { readonly type: "boolean"; }; }
readonly contents: Map<string, number>
onAttach()
get usable(): booleanis it open for use?
get total(): number
count(item: string): number
put(actor: Entity, item: string, n = 1): boolean
take(actor: Entity, item: string, n = 1): boolean
toJSON(): Record<string, number>
load(data: Record<string, number> | undefined)

packages/interaction/src/container.ts:19

Door class

class Door extends Component<DoorParams>

A door that opens and closes with a hinge or a slide. It blocks characters while it is (mostly) closed: its meshes keep checkCollisions on until it is nearly open. Events: door:open, door:close, door:locked { door, actor }, door:unlocked.

schema: { readonly open: { readonly type: "boolean"; }; readonly locked: { readonly type: "boolean"; }; readonly key: { readonly type: "string"; }; readonly consumeKey: { readonly type: "boolean"; }; readonly angle: { readonly type: "number"; readonly min: -360; readonly max: 360; }; readonly slide: { readonly type: "array"; }; readonly duration: { readonly type: "number"; readonly min: 0.05; readonly max: 30; }; readonly openPrompt: { readonly type: "string"; }; readonly closePrompt: { readonly type: "string"; }; readonly with: { readonly type: "array"; }; readonly range: { readonly type: "number"; readonly min: 0.5; readonly max: 50; }; readonly interact: { readonly type: "boolean"; }; readonly overlap: { readonly type: "object"; }; }what the editor checks params against
isOpen: boolean
locked: boolean
t: number0 closed .. 1 open
onAttach()
use(actor: Entity)
unlock()
lock()
setOpen(open: boolean, instant = false, together = true)
onFixedUpdate(dt: number)

packages/interaction/src/props.ts:51

Grabbable class

class Grabbable extends Component<GrabbableParams>

A physics body that a hand takes: it follows the hand by a velocity drive, so heavy objects lag and a wall in the way stops them; on release it keeps the hand's velocity of the last 100 ms (a throw). On the screen the use action holds it in front of the camera. Needs a RigidBody on the entity. Events: grab:take { target, by }, grab:release { target, by, velocity }.

schema: { readonly maxMass: { readonly type: "number"; readonly min: 0; }; readonly hands: { readonly type: "string"; }; readonly twoHanded: { readonly type: "boolean"; }; readonly snapPoints: { readonly type: "array"; }; readonly stiffness: { readonly type: "number"; readonly min: 0; }; readonly maxSpeed: { readonly type: "number"; readonly min: 0; }; readonly screenHold: { readonly type: "number"; readonly min: 0; }; }
onAttach()
get held(): boolean
get holders(): string[]
canGrab(id: string): booleanMay this hand take it now?
grab(id: string, holder: Holder): booleanTake it with a holder (a hand). Returns false when it cannot be taken.
release(id: string, throwIt = true): booleanLet go; the body keeps the hand's velocity from the last 100 ms.
onFixedUpdate(dt: number)
onDetach()

packages/interaction/src/grab.ts:43

Interactable class

class Interactable extends Component<InteractableParams>

Something the player can use. Behaviours (Door, Pickup ...) hook in with onUse; games can listen to the interact:use event.

available: booleanSet false to make it unavailable (a locked-out prompt, a finished quest).
prompt: stringa prompt the owner changes at run time ("Close door" when it is open)
onAttach()
get range(): number
get usable(): boolean
onUse(fn: UseHandler): this
interact(actor: Entity): booleanUse it. Returns false if it could not be used now.

packages/interaction/src/interact.ts:19

Interactor class

class Interactor extends Component<InteractorParams>

Put on the player. Each fixed step it picks the usable thing the player is closest to looking at (within the thing's range), announces changes with interact:focus / interact:blur, and uses the focus when the action is pressed.

focus: Interactable | null
view: () => { origin: BABYLON.Vector3; dir: BABYLON.Vector3 }The view the focus is chosen by. Follows the camera rig of a PlayerController; replace it to drive the view yourself.
readonly sources: FocusSource[]focus sources besides the view cone, in order of priority
via: stringwhich source holds the focus now
onFixedUpdate()

packages/interaction/src/interact.ts:89

Inventory class

class Inventory extends Component<{ items?: Record<string, number> }>

What an actor carries: item id -> count. Doors ask it for keys, pickups fill it. Events: inventory:change { entity, item, count }.

readonly items: Map<string, number>
onAttach()
count(item: string): number
has(item: string, n = 1): boolean
add(item: string, n = 1): number
remove(item: string, n = 1): booleanTake n of an item; false (and nothing taken) if there are fewer.
toJSON(): Record<string, number>Plain data for a save (levels.state.inventory = inv.toJSON()).
load(data: Record<string, number> | undefined)

packages/interaction/src/inventory.ts:4

LevelDoor class

class LevelDoor extends Component<LevelDoorParams>

A way into another level: using it travels there (see

schema: { readonly level: { readonly type: "string"; readonly required: true; }; readonly spawn: { readonly type: "string"; }; readonly prompt: { readonly type: "string"; }; readonly range: { readonly type: "number"; readonly min: 0.5; readonly max: 50; }; readonly key: { readonly type: "string"; }; }
onAttach()

packages/interaction/src/props.ts:167

Lever class

class Lever extends Component<LeverParams>

A switch. Using it flips it and toggles the doors it points at (opening a locked one unlocks it). Event: lever:toggle { lever, on }.

schema: { readonly targets: { readonly type: "array"; }; readonly on: { readonly type: "boolean"; }; readonly onPrompt: { readonly type: "string"; }; readonly offPrompt: { readonly type: "string"; }; readonly range: { readonly type: "number"; readonly min: 0.5; readonly max: 50; }; }
on: boolean
onAttach()
set(on: boolean)

packages/interaction/src/props.ts:212

Pickup class

class Pickup extends Component<PickupParams>

An item lying around: using it moves it into the actor's Inventory (made if missing) and removes it. Event: pickup:collected { item, count, actor, pickup (the entity's id) }.

schema: { readonly item: { readonly type: "string"; readonly required: true; }; readonly count: { readonly type: "number"; readonly min: 1; }; readonly prompt: { readonly type: "string"; }; readonly range: { readonly type: "number"; readonly min: 0.5; readonly max: 50; }; }
onAttach()

packages/interaction/src/props.ts:188

Functions

installInteraction function

function installInteraction(world: World): void

Register the interaction components with a world so manifests and entity.add('Door', ...) can name them.

packages/interaction/src/index.ts:15

Interfaces

ContainerParams interface

interface ContainerParams
capacity?: numbermost items it holds, counting every unit (default: unlimited)
items?: Record<string, number>what it holds to begin with: item id -> count
alwaysOpen?: booleanlet it be used with the door shut (default false: with a Door on the same entity, only while that is open)

packages/interaction/src/container.ts:5

DoorParams interface

interface DoorParams
open?: boolean
needsSight?: booleanfalse: the prompt/use works through the door's own collision (see InteractableParams.needsSight)
locked?: booleanlocked until the actor holds this item (see Inventory); locked: true without a key never opens by use
key?: string
consumeKey?: booleanuse the key up when unlocking
angle?: numberhinge doors: degrees the panel turns about its own y axis (default 90; negative swings the other way)
slide?: [number, number, number]sliding doors: the offset of the open position in the node's local frame, metres. Used instead of the hinge when set.
duration?: numberseconds to open or close (default 0.6)
openPrompt?: stringthe open/close prompts
closePrompt?: string
range?: number
with?: string[]entity ids of doors that open and close together with this one (the two leaves of a gate); each keeps its own prompt and parameters
interact?: booleanfalse: no prompt and no use (a door that only overlap moves)
overlap?: { radius: number; centre?: [number, number, number]; tags?: string[]; pad?: number; /** the sphere's place relative to the door node as Unreal gives a component's relative location (cm, X forward, Y right, Z up); used when `centre` is not given */ ueOffset?: [number, number, number]; }Opens when an entity with one of tags (default player, npc) comes within radius m of centre (world position; default the door's own) and closes when one leaves: each entering opens, each leaving closes, as an Unreal sphere's begin/end overlap that plays/reverses a timeline does. pad (m, default 0.35) is the visitor's own radius (a capsule overlaps before its centre is inside).

packages/interaction/src/props.ts:12

FocusEvent interface

interface FocusEvent
target: Entity
actor: Entity
prompt: string
via?: string

packages/interaction/src/interact.ts:73

FocusSource interface

interface FocusSource

Another way to focus something besides the view cone: a hand's ray, a hand touching. pick returns the usable thing it points at or touches; pressed is its own use button (trigger, a touch) this step. A source that picks something wins over the cone.

name: string
pick(actor: Entity): Interactable | null
pressed(): boolean

packages/interaction/src/interact.ts:79

GrabbableParams interface

interface GrabbableParams
maxMass?: numberkg: heavier objects cannot be taken (default 40)
hands?: 'left' | 'right' | 'both'which hand may take it (default both)
twoHanded?: booleantwo hands may hold it at once (default false)
snapPoints?: Array<{ position: [number, number, number]; rotation?: [number, number, number] }>handles in the object's frame: a hand takes the object so the nearest snap point sits in the palm (position, and an optional rotation as Euler angles)
stiffness?: numberhow hard the hold pulls toward the hand, 1/s (default 18); heavy objects lag because the pull speed is capped by maxSpeed scaled down with mass
maxSpeed?: numberm/s at 1 kg or less (default 6): heavier bodies are slower, so they lag and cannot be dragged through walls by the hand
screenHold?: number | falsethe screen version: metres in front of the camera the object is held at (default 1.4); false = the use action does not pick it up

packages/interaction/src/grab.ts:11

Holder interface

interface Holder

Where a hand (or the screen's "hold in front of the camera") wants the object: world space.

pose(): { position: BABYLON.Vector3; rotation: BABYLON.Quaternion } | nullnull while the hand is not tracked: the object is let go of

packages/interaction/src/grab.ts:6

InteractableParams interface

interface InteractableParams
prompt?: stringthe text the prompt shows ("Open door"); the UI adds the button label
range?: numberhow close the actor must be, metres (default 2.5)
cooldown?: numberseconds before it can be used again
once?: booleancan only be used once
needsSight?: booleanfalse: usable through a solid between the eyes and it (furniture whose own collision box surrounds it); the range still applies

packages/interaction/src/interact.ts:3

InteractorParams interface

interface InteractorParams
action?: stringthe button action (default "use")
cone?: numberhalf-angle of the cone in front of the view that counts as "looking at", degrees (default 35)
eyeHeight?: numberheight of the eyes for the view ray when there is no camera rig, metres
needsSight?: booleanobjects between the viewer and the target block it (static level geometry; default true)
sightSlack?: numbermetres just before the target where a hit does not block: the target's own housing (a locker door sits in the cabinet's collision box, a button in its panel). Default 0.25

packages/interaction/src/interact.ts:48

LevelDoorParams interface

interface LevelDoorParams
level: string
spawn?: string
prompt?: string
range?: number
key?: stringheld item needed to pass

packages/interaction/src/props.ts:157

LeverParams interface

interface LeverParams
targets?: string[]entity ids whose Door is toggled
on?: boolean
onPrompt?: string
offPrompt?: string
range?: number

packages/interaction/src/props.ts:202

PickupParams interface

interface PickupParams
item: string
count?: number
prompt?: string
range?: number

packages/interaction/src/props.ts:180

Types

UseHandler type

type UseHandler = (actor: Entity) => void

packages/interaction/src/interact.ts:16