@adatechnology/game-interactables

中文文档
0.2.17发布于 2026/11/06MITTypeScriptES Module构建来源已验证近 30 天下载 4044

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.

原始使用文档
文档目录9 节
  1. Install
  2. Usage
  3. Behaviour
  4. API
  5. Events
  6. Locales
  7. Destruction
  8. Português
  9. Destruction delegate

@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.

GroupMembers
Objectsadd(id, spec, transform), remove(id), get(id), objects()
Frameupdate(dt)
Interactioninteractables(), target(eye, TargetOptions), act(id, actor?)
PartspartT, setPart(object, part, target, instant?), fitSweep(object, part, blocked)
DrivesetDevice(object, device, on), setFixture(object, fixture, on), setBurner(object, burner, on)
Containerscontents, accessible, take, put, slotPosition
Powerpower.grid, setGrid, setBreaker, breaker, isPowered, isOn, pluggedInto, channel
FuelfuelLevel(object, fuel), isLit(object, burner)
Waterwater.setDefaultTank, level, refill, setRain, isFlowing
Seatssit, stand, seatOf
Softpush, sway, setWind
Drawemitters(), mirrors(eye, maxDistance?, count?), colliders({ merge? })
Damagedamage(object, target, amount, kind?), hp(object, target)
Textt(key, vars?), setLocale(locale)
Saveserialize(), 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:

typeFields
partobject, part, t, target (on a new target and again on arrival)
pickupobject, container, slot, stack, actor
deviceobject, device, on, powered, channel?
switchobject, switch, on
fixtureobject, fixture, on, flowing
burnerobject, burner, on, lit
seatobject, seat, actor, seated
damageobject, target, hp, destroyed
explosionobject, pos, radius
leakobject, target, pos
messagekey, text (localized): no_power, no_gas, no_water, locked, empty, full, or an item text key from a read
powergrid
containerobject, 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.

版本与发布记录

@adatechnology/game-interactables 共发布 20 个版本,首次发布于 2026/10/23;过去 12 个月发布了 20 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.2.17latest2026/11/06今天
0.2.162026/11/042 天前
0.2.152026/11/042 天前
0.2.142026/11/042 天前
0.2.132026/11/033 天前
0.2.122026/11/015 天前

关于 @adatechnology/game-interactables 的常见问题

@adatechnology/game-interactables 是什么?

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.

如何安装 @adatechnology/game-interactables?

在项目目录运行 npm install @adatechnology/game-interactables,或使用 pnpm add / yarn add / bun add。

@adatechnology/game-interactables 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@adatechnology/game-interactables 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@adatechnology/game-interactables 可以商用吗?

@adatechnology/game-interactables 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@adatechnology/game-interactables 的下载量有多少?

根据 npm 官方下载统计,@adatechnology/game-interactables 在 2026/10/05 – 2026/11/03 这 30 天内被下载 4,044 次,日均约 135 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@adatechnology/game-interactables 还在维护吗?

最新版本 0.2.17 发布于 2026/11/06(今天);至今共发布 20 个版本,其中过去 12 个月发布了 20 个;首次发布于 2026/10/23。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。