npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@adatechnology/game-interactables

v0.2.17

Published

Renderer-agnostic runtime for interactive world objects: movable parts, containers with pickable items, power grid, fuel, logical plumbing, seats, soft volumes, effects, mirrors and damage.

Readme

@adatechnology/game-interactables

Renderer-agnostic runtime for interactive world objects: movable parts (doors, drawers, lids, knobs), containers with pickable items and deterministic loot, a logical power grid (meter, circuits, sockets, switches, devices), fuel (gas bottles, charcoal), a logical water network, seats and beds, soft volumes that sway instead of blocking, effect emitters, mirrors, colliders and damage.

The package draws nothing. Content packs describe objects as ObjectSpec data (see src/types.ts), the host adds them to a World, calls update(dt) every frame and draws whatever the world reports (part poses, active emitters, mirrors, colliders, sway). three is not required.

Install

pnpm add @adatechnology/game-interactables

Browser without a bundler (the global is GameInteractables):

<script src="https://unpkg.com/@adatechnology/game-interactables/dist/index.global.js"></script>
<script>
  const world = GameInteractables.createWorld({ seed: 'level-1' })
</script>

Usage

import { createWorld, poseOf, type ObjectSpec } from '@adatechnology/game-interactables'

const world = createWorld({
  locale: 'en',
  seed: 'level-1',
  items: { can: { id: 'can', category: 'food', size: [0.07, 0.11, 0.07], weight: 0.4, stack: 6 } },
  loot: { pantry: { fill: 0.7, entries: [{ type: 'can', weight: 1, qty: [1, 3] }] } },
  strings: { en: { 'prompt.door.closed': 'Open {name}', 'prompt.door.open': 'Close {name}', 'type.cupboard': 'cupboard' } },
  onEvent: (e) => console.log(e),
})

const cupboard: ObjectSpec = {
  type: 'cupboard',
  parts: [{ id: 'door', pivot: [-0.4, 0, 0.3], motion: { kind: 'hinge', angle: -Math.PI / 2 }, sweep: [-0.4, 0.3, 0.4, 1.1], label: 'prompt.door' }],
  containers: [{ id: 'main', access: 'door', volume: { center: [0, 0.5, 0], half: [0.4, 0.4, 0.25] }, slots: [{ pos: [0, 0.1, 0], max: [0.3, 0.3, 0.3] }], loot: 'pantry' }],
}
world.add('kitchen_cupboard', cupboard, { x: 2, y: 0, z: -1, ry: Math.PI })

// once, after placing objects next to walls: the door never opens through them
world.fitSweep('kitchen_cupboard', 'door', (x, z) => isWall(x, z))

// every frame
world.update(dt)
const look = world.target({ x: cam.x, y: cam.y, z: cam.z, yaw, pitch }) // best interaction or null
if (look) hud.show(look.label) // "Open cupboard"
if (pressedUse && look) look.act()

// drawing a part: pose is relative to the part's pivot
for (const o of world.objects()) {
  for (const p of o.spec.parts ?? []) {
    const pose = poseOf(p.motion, world.partT(o.id, p.id)) // { position, rotation: { axis, angle }, scale }
  }
}

Coordinates in a spec are local to the object: front is +z, origin at the center of the footprint on the floor, meters. Transform.ry rotates around +y like three.js (+z turns toward +x for a positive angle). View yaw follows the camera convention: yaw 0 looks toward -z.

Behaviour

  • Parts animate toward their target at motion.speed (default 2.5 t/s). requires keeps a part from opening while another one is not open and closes it with that one (a drawer behind a door closes with the door). fitSweep(object, part, blocked) samples the swept sweep rectangle (rotating the leaf for hinges, translating for slides), returns the largest safe t (when a part declares bounds, the whole closed-pose slab is moved through the motion, so leaf thickness, hinge offset and horizontal hinge axes count) and clamps the part to it for good. A part counts as open from t >= 0.5 (or 90% of its clamped limit). Parts that share a group move together: setPart or a toggle on any member sets the target of all (blind slats, curtain panels).
  • Default interactions: when a spec has no interactions, the world derives them. Labelled parts give toggle_part; containers without an access part give open_container; devices, switches, fixtures, burners (and ignite for needsIgnition), gas valves and seats get the matching action. Explicit interactions replace the derived ones. Ids are part.<id>, open.<container>, device.<id>, channel.<id>, switch.<id>, fixture.<id>, burner.<id>, ignite.<id>, fuel.<id>, seat.<id>; world-wide object:interactionId.
  • Labels: the world looks up <label>.open|.closed (parts, valves, containers) or <label>.on|.off (devices, fixtures, burners, switches), then <label>, then an engine default (prompt.open, prompt.turn_on, ...). {name} is replaced by type.<type> or a humanized type id. Labels never carry a key hint such as "E:"; the host adds its own.
  • Structured prompts: every Interactable also has verb ("Open") and noun ("the fridge") so a host can style them separately (object is the owning object's id). Entries <key>.verb / <key>.object in the locale win; otherwise the first word of the label is the verb, the rest the noun, and a one-word prompt uses the object's type name as the noun.
  • Availability: Interactable.available is false with a reason (a locale key: requires_open, no_power, locked when canInteract refuses, already_lit, needs_ignition, turn_on_first; world.t(reason) is the text) while the interaction cannot be used.
  • Targeting: target(eye, { reach = 1.8, cone = 0.45, filter?, kinds?, includeUnavailable? }) scores by distance and angle to the view direction. Unavailable interactions (closed container take/read, the wrong half of ignite, a part gated by requires...) are skipped unless includeUnavailable; kinds limits the action kinds and filter is any predicate. A hard-wired lamp (device kind: 'lamp' with a circuit, no plug, no part) gets no derived interaction: switches and setDevice drive it, so it never steals the interact key.
  • Driving state from code: setDevice(object, device, on), setFixture(object, fixture, on) and setBurner(object, burner, on) do what the toggles do (parts, device/fixture/burner events, no_power message) and are quiet when the state does not change.
  • Colliders: colliders({ merge: true, gap = 0.02, slack = 0.1 }) unions overlapping or adjacent fixed boxes of the same object (same rotation) when the merged box adds at most slack extra volume; door colliders stay separate. mergeColliders(list, opts) is exported for lists you already have.
  • Containers: contents from contents or deterministic loot seeded with seed + object id + container id. An item fits a slot when its size is within the slot's max (width and depth may swap). take needs the container to be accessible and the item pickable and emits pickup.
  • Power: grid, a breaker per circuit (default on), sockets (optionally on a circuit) and devices plugged to a socket (auto = nearest free socket within 2 m, across the whole world) or hard-wired to a circuit. powered = grid && breaker && socket found. A device that is on but unpowered stays on, reports powered: false and emits the no_power message. Switches toggle the listed devices (object:device or local ids). The toggle_breaker action ({ do: 'toggle_breaker', circuit, part? }) flips a circuit from a breaker panel, mirrors the optional rocker part (t = 1 when on; refused while that part is gated by PartSpec.requires, e.g. a closed panel door) and uses the prompts prompt.breaker_on|off ({circuit} is the action's name or the circuit id).
  • Fuel: burners need fuel and, for a bottle with a valve, the valve open. fuel: 'auto' = nearest gas source within 2 m. Gas is consumed while lit; running out or closing the valve puts the flame out, and a gas knob left on without gas lights by itself when the valve opens. Charcoal needs ignite, burns until empty, and toggle_burner puts it out. Smoke emitters of solid fuel linger 20 s after the fire is out, fading.
  • Water: tanks (capacity, level, rainArea), a default tank, fixtures that flow while on and supplied. With options.waterSupply, fixtures draw through it (flow / 60 * dt liters, a flush draws volume once). Dry fixtures stay "on" but report flowing: false. A device with fixture: '<fixtureId>' (same object, e.g. a washing machine inlet) drives that fixture: it flows while the device is on and powered, stops when it is switched off or loses power, and gets no interaction of its own.
  • Gated interactions: InteractionSpec.requires = { object, part } (world-wide object id) makes an interaction available only while that part is open (t >= 0.5): target skips it and act refuses it otherwise (a gas valve behind a cabinet door).
  • Seats: one actor per seat. sit returns the world eye position and yaw, stand returns the world exit point.
  • Soft volumes never block and are never colliders. push(pos, radius, vel) swings them; they settle back by a damped spring. Wind adds a small oscillating force. A panel with a part opens when pushed from the front.
  • Damage: hp per target. A destroyed gas source with fuel explodes (radius = 2 + level / 5) and empties; a damaged tank leaks once and drains; glass is just an event.
  • Save/load: serialize() is plain JSON; load() restores it onto a world that has the same objects added. Swept limits (fitSweep) are not saved, call it again after loading.

API

createWorld(options?: WorldOptions): World

WorldOptions: locale, strings, items, loot, seed, grid, rain, wind, waterSupply, onEvent, canInteract.

| Group | Members | |---|---| | Objects | add(id, spec, transform), remove(id), get(id), objects() | | Frame | update(dt) | | Interaction | interactables(), target(eye, TargetOptions), act(id, actor?) | | Parts | partT, setPart(object, part, target, instant?), fitSweep(object, part, blocked) | | Drive | setDevice(object, device, on), setFixture(object, fixture, on), setBurner(object, burner, on) | | Containers | contents, accessible, take, put, slotPosition | | Power | power.grid, setGrid, setBreaker, breaker, isPowered, isOn, pluggedInto, channel | | Fuel | fuelLevel(object, fuel), isLit(object, burner) | | Water | water.setDefaultTank, level, refill, setRain, isFlowing | | Seats | sit, stand, seatOf | | Soft | push, sway, setWind | | Draw | emitters(), mirrors(eye, maxDistance?, count?), colliders({ merge? }) | | Damage | damage(object, target, amount, kind?), hp(object, target) | | Text | t(key, vars?), setLocale(locale) | | Save | serialize(), load(state) |

Standalone helpers: rng(seed), generateLoot(tables, tableId, slots, seed, items), poseOf(motion, t), localToWorld(transform, v), worldToLocal, rotY, rotateAxis, fitsSize, fitsSlot, resolveStrings(locale, extra), locales, format, pickLocale.

poseOf(motion, t) returns { position, rotation: { axis, angle }, scale } relative to the part's pivot: hinge, rocker and knob rotate, slide translates along axis * distance * t, scale scales along axis.

Events

onEvent receives every change:

| type | Fields | |---|---| | part | object, part, t, target (on a new target and again on arrival) | | pickup | object, container, slot, stack, actor | | device | object, device, on, powered, channel? | | switch | object, switch, on | | fixture | object, fixture, on, flowing | | burner | object, burner, on, lit | | seat | object, seat, actor, seated | | damage | object, target, hp, destroyed | | explosion | object, pos, radius | | leak | object, target, pos | | message | key, text (localized): no_power, no_gas, no_water, locked, empty, full, or an item text key from a read | | power | grid | | container | object, container, open (an open shelf was "opened") |

Locales

Engine-level strings exist in en and pt-BR: prompt.open, prompt.close, prompt.turn_on, prompt.turn_off, prompt.take, prompt.sit, prompt.lie_down, prompt.get_up, prompt.light, prompt.change_channel, prompt.read, prompt.open_valve, prompt.close_valve and the messages above. Content packs add their keys through options.strings; unknown keys fall back to English, then to the key itself.

Destruction

Specs carry tags for the shared destruction SDK, @adatechnology/game-destruction; this package does not depend on it and only stores plain ids.

  • PartSpec.destruction: { material, joint?, piece?, explodes?, contains? }. material is an SDK material id (steel, mdf, glass...), joint an SDK joint kind (screw, bolt, nail, weld, glue, hinge, clip, tie, mortar, fused, stem), piece what the part is (door, drawer, lid...). explodes and contains have the same shape as in the SDK's PartSpec.
  • ObjectSpec.destruction: the same tags for the static body (everything that is not a movable part).

The host builds SDK parts from the tags and routes hits, fire and blasts to the SDK. DamageSpec and world.damage are deprecated: they keep working, but get no new features.

Português

Runtime sem renderer para objetos interativos do mundo: portas, gavetas e tampas que se movem, recipientes com itens e loot determinístico por semente, rede elétrica lógica (medidor, circuitos, tomadas, interruptores, aparelhos), combustível (botijão de gás, carvão), rede de água lógica, assentos e camas, volumes macios (cortinas, folhagem) que balançam sem bloquear, emissores de efeitos, espelhos, colisores e dano.

const world = createWorld({ locale: 'pt-BR', seed: 'fase-1' })
world.add('armario', especificacao, { x: 2, y: 0, z: -1, ry: Math.PI })
world.update(dt)
const alvo = world.target({ x, y, z, yaw, pitch }) // ex.: "Abrir armário"
alvo?.act()

O pacote não desenha nada: você informa o ObjectSpec (dados), chama update(dt) a cada quadro e desenha o que o mundo reportar. Os textos do motor (Abrir, Ligar, Sem energia, Sem gás, Sem água...) vêm em pt-BR; os pacotes de conteúdo acrescentam as suas chaves em strings.

Destruction delegate

Pass destruction: { hitPart } (the shape of DestructionWorld.hitPart from @adatechnology/game-destruction, no dependency on it) to createWorld and world.damage forwards to it as a blunt (explosive for gas, slash for fabric) hit with energy: amount; the world stops its own hp bookkeeping and world.hp returns null. Without it the deprecated DamageSpec behaviour is unchanged.