Fluffer by MotorbunnyDevelopers

Web Launcher SDK

Overview

Build HTML5 and Three.js apps that run inside Fluffer and drive the player's toy through the launcher bridge. What the SDK packages are and how apps run.

Fluffer is a phone app (iOS and Android) that hosts small interactive apps and games and connects them to the player's Bluetooth toy. You build your app with ordinary web technology: HTML, CSS, TypeScript or JavaScript, Canvas or Three.js. Fluffer takes care of pairing, Bluetooth, permissions and the app lifecycle. Your code asks for a buzz, and the toy buzzes.

The Web Launcher SDK is the set of packages and conventions that make this work.

#What you can build

#How an app runs inside Fluffer

  1. The player opens your app from Fluffer's catalog.
  2. Fluffer loads your bundle in a web view. Your app is served from its own address (https://<your-app>.fluffer.app/), so its storage is never shared with other apps.
  3. Before your first frame, Fluffer injects a small bridge into the page. The bridge is a JSON request/response channel between your code and the launcher, called fluffer-bridge/1.
  4. Your app performs a handshake: it says which capabilities it wants (for example device.vibrate), and Fluffer answers with the capabilities it actually granted, plus the connected toy, if any.
  5. From then on your app sends requests (device.setAllMotors, host.exit, ...) and receives events (device.connectionChanged, tips.received, ...).
  6. When the player backgrounds Fluffer, per-frame work in game.onTick stops with the page. When they leave your app, Fluffer closes it and the toy stops, so save progress as you go.

Your app never touches Bluetooth and never asks the OS for Bluetooth permission. The capabilities you can use are the ones your manifest declares and the launcher grants.

#The SDK packages

PackageWhat it is forUse it when
@fluffer/game-kitThe recommended framework. One call (createGame()) does the handshake, frame loop, pointer and keyboard input, auto-pause, 14 named haptic primitives, 25 haptic recipes, reactive state, on-device storage, and helpers for tips, media and accounts.You're building a game or interactive app. Start here.
@fluffer/launcher-sdkThe lower-level bridge client. Exposes Scanner, LauncherDevice, host, user, the manifest validator and a mock launcher for browser development and tests.You want raw device control, are porting an app written for the standalone Fluffer SDK, or are writing tests.
fluffer-sdkThe standalone Fluffer SDK for apps that talk to a toy directly over Bluetooth, outside the launcher. @fluffer/launcher-sdk mirrors its API and re-exports the types you need.You don't need it for launcher apps. It only matters if you're moving an existing standalone app into Fluffer.

@fluffer/game-kit is built on @fluffer/launcher-sdk and re-exports the pieces a game needs (host, user, isInLauncher, FlufferError, FlufferErrorCode and the common types), so most apps import from one place.

#A minimal app

This is a complete Fluffer app: tap the screen and the toy gives a short buzz.

import { createGame, haptics } from '@fluffer/game-kit';

const game = await createGame({
  capabilities: ['device.vibrate'],
  canvas: document.querySelector('canvas'),
});

game.onPointer((e) => {
  if (e.type === 'down') void haptics.impact(0.7);
});

It needs a manifest.json next to it:

{
  "manifestVersion": 1,
  "minSdkVersion": "0.1.0",
  "engine": "html5",
  "entry": "index.html",
  "orientation": "landscape",
  "capabilities": ["device.vibrate"]
}

In a plain browser tab, createGame() notices it isn't inside Fluffer and installs a mock launcher with a virtual toy, so the same code runs on your laptop without hardware.

#The same thing with the lower-level SDK

import { Scanner, host } from '@fluffer/launcher-sdk';

const scanner = new Scanner({ requestedCapabilities: ['device.vibrate'] });
await scanner.init();                          // handshake with the launcher

const [info] = await scanner.getDevices();     // the toy the player already paired
if (info) {
  const device = await scanner.createDevice(info);
  await device.setAllMotors({ m1: 180, m2: 180, m3: 0, m4: 0 });  // 0-200 per motor
}

setTimeout(() => host.exit(), 30_000);

#Where to go next