@ferret/interaction
Interactables with focus and prompts, inventory and stock props: doors, level doors, pickups, levers.
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(): boolean | is 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: number | 0 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): boolean | May this hand take it now? |
grab(id: string, holder: Holder): boolean | Take it with a holder (a hand). Returns false when it cannot be taken. |
release(id: string, throwIt = true): boolean | Let 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: boolean | Set false to make it unavailable (a locked-out prompt, a finished quest). |
prompt: string | a 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): boolean | Use 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: string | which 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): boolean | Take 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): voidRegister 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 ContainerParamscapacity?: number | most items it holds, counting every unit (default: unlimited) |
items?: Record<string, number> | what it holds to begin with: item id -> count |
alwaysOpen?: boolean | let 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 DoorParamsopen?: boolean | |
needsSight?: boolean | false: the prompt/use works through the door's own collision (see InteractableParams.needsSight) |
locked?: boolean | locked until the actor holds this item (see Inventory); locked: true without a key never opens by use |
key?: string | |
consumeKey?: boolean | use the key up when unlocking |
angle?: number | hinge 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?: number | seconds to open or close (default 0.6) |
openPrompt?: string | the 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?: boolean | false: 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 FocusEventpackages/interaction/src/interact.ts:73
FocusSource interface
interface FocusSourceAnother 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 GrabbableParamsmaxMass?: number | kg: heavier objects cannot be taken (default 40) |
hands?: 'left' | 'right' | 'both' | which hand may take it (default both) |
twoHanded?: boolean | two 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?: number | how 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?: number | m/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 | false | the 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 HolderWhere a hand (or the screen's "hold in front of the camera") wants the object: world space.
pose(): { position: BABYLON.Vector3; rotation: BABYLON.Quaternion } | null | null while the hand is not tracked: the object is let go of |
packages/interaction/src/grab.ts:6
InteractableParams interface
interface InteractableParamsprompt?: string | the text the prompt shows ("Open door"); the UI adds the button label |
range?: number | how close the actor must be, metres (default 2.5) |
cooldown?: number | seconds before it can be used again |
once?: boolean | can only be used once |
needsSight?: boolean | false: 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 InteractorParamsaction?: string | the button action (default "use") |
cone?: number | half-angle of the cone in front of the view that counts as "looking at", degrees (default 35) |
eyeHeight?: number | height of the eyes for the view ray when there is no camera rig, metres |
needsSight?: boolean | objects between the viewer and the target block it (static level geometry; default true) |
sightSlack?: number | metres 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 LevelDoorParamslevel: string | |
spawn?: string | |
prompt?: string | |
range?: number | |
key?: string | held item needed to pass |
packages/interaction/src/props.ts:157
LeverParams interface
interface LeverParamstargets?: string[] | entity ids whose Door is toggled |
on?: boolean | |
onPrompt?: string | |
offPrompt?: string | |
range?: number |
packages/interaction/src/props.ts:202
PickupParams interface
interface PickupParamsitem: string | |
count?: number | |
prompt?: string | |
range?: number |
packages/interaction/src/props.ts:180
Types
UseHandler type
type UseHandler = (actor: Entity) => voidpackages/interaction/src/interact.ts:16