Fluffer by MotorbunnyDevelopers

Web Launcher SDK

Getting started

Scaffold a Fluffer app, run it in the browser against a mock toy, test haptics without hardware, and package it for upload.

This guide takes you from nothing to a Fluffer app running in your browser, then to an upload-ready package. It takes about five minutes.

#What you need

#1. Get the SDK and scaffold an app

The SDK packages (@fluffer/game-kit, @fluffer/launcher-sdk) aren't on the npm registry yet. During the developer preview we share the SDK repository with each developer: email developers@fluffer.app with a line about what you’re building and we’ll send you access. Apps are created inside that repository; a script copies a starter template and wires everything up for you.

# in the SDK repository we share with you
npm install
npm run build                      # builds the SDK packages your app imports
npm run create-game -- --slug=my-game
npm install                        # links the new app into the workspace
cd games/my-game
npm run dev

Open the URL Vite prints. Your app is running against a mock launcher with a virtual toy.

#Options for create-game

OptionValuesDefault
--slugLowercase letters, digits and dashes, e.g. my-game. Required.none
--templatehtml5-vanilla (2D canvas) or threejs (3D / WebGL)html5-vanilla
--nameDisplay name, e.g. "Bunny Dash"derived from the slug
--capabilitiesComma-separated capability tokens, e.g. device.vibrate,device.rotatethe template's
--orientationlandscape, portrait or autothe template's
npm run create-game -- --slug=bunny-dash --template=threejs --capabilities=device.vibrate,device.rotate --orientation=landscape

The script gives the app its own dev-server port (5174 or higher), sets up manifest.json from your options and creates a public/ folder for assets.

#2. What's in your app

games/my-game/
  src/main.ts        # your code; imports from @fluffer/game-kit
  public/            # images, audio, fonts; copied as-is into dist/
  index.html         # a full-screen <canvas> that loads src/main.ts
  manifest.json      # what your app needs from Fluffer
  package.json       # dev / build / package scripts
  vite.config.ts     # base './' and target es2022: keep both

Two settings in vite.config.ts matter. base: './' makes asset URLs relative so the bundle works wherever it's hosted. target: 'es2022' allows top-level await, which await createGame() relies on.

#3. Write your first game loop

Replace src/main.ts with this:

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

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

const state = game.state(
  { score: 0, highScore: 0 },
  { persist: ['highScore'] },          // survives reloads
);

game.onTick((dt) => {
  // update + render; dt is milliseconds since the last frame.
  // Paused automatically when Fluffer goes to the background.
});

game.onPointer((e) => {
  if (e.type !== 'down') return;
  state.current.score += 1;
  void haptics.tap();                              // small acknowledgement
  if (state.current.score % 10 === 0) void haptics.success();
  if (state.current.score > state.current.highScore) {
    state.current.highScore = state.current.score; // saved automatically
  }
});

game.onKey((e) => {
  if (e.type === 'keydown' && e.key === 'Escape') void game.exit();
});

Save and Vite hot-reloads.

#What createGame() gives you

MemberWhat it does
game.status'initialising', 'ready', 'paused', 'fuel-empty', 'exited' or 'error'
game.deviceThe connected toy (LauncherDevice), or null when there isn't one
game.capabilitiesThe capabilities Fluffer actually granted. Asking isn't a guarantee.
game.onTick((dt, total) => ...)Frame loop driven by requestAnimationFrame; auto-paused
game.onPointer((e) => ...)Mouse, touch and pen in one stream. e.type is 'down', 'move' or 'up'; e.nx and e.ny run from -1 to 1 with 0 at the centre; e.pressed is true while held.
game.onKey((e) => ...)Keyboard events (keydown and keyup); auto-paused
game.state(initial, options)Reactive state with optional persistence
game.storageOn-device documents and files (Storage)
game.tips, game.media, game.authTips, saving to the photo library, accounts (Bridge, Accounts)
game.exit()Asks Fluffer to close your app

Every on* method returns an unsubscribe function.

#createGame() options

OptionTypeDefaultNotes
capabilitiesCapability[]['device.vibrate']Requested at handshake. Must also be declared in your manifest.
canvasHTMLCanvasElementnonePointer coordinates are normalised to this element
slugstring'mock-game'Your app's slug. Names your storage database and the audience of mock account tokens.
autoMockbooleantrueInstall the mock launcher when not running inside Fluffer
maxMotorCallsPerSecondnumber50Excess motor calls are coalesced: the most recent value wins
pauseOnEmptyFuelbooleanfalseAuto-pause when tip fuel runs out (needs tips.fuel)
resumeFuelThresholdnumber1Fuel level that ends a fuel-empty pause
sdkVersionstringSDK versionOverride the version sent in the handshake

#4. Test without a toy

#In the browser (always on)

When your app isn't running inside Fluffer, createGame() installs a mock launcher automatically. The mock answers the handshake with a virtual MBBunny (four motors and rotation), grants the capabilities you asked for, and accepts every call. You can build and tune most of your app this way.

#See the motors with the mock monitor

The SDK repository includes a visualiser at examples/mock-monitor. It shows live motor levels, rotation, an intensity chart and a log of every bridge call. It also has buttons to simulate the toy connecting and disconnecting, and a tips simulator.

Start it in a second terminal:

cd examples/mock-monitor && npm run dev     # runs on port 5181

Add this to the top of your src/main.ts. It only runs in development and is left out of production builds:

if (import.meta.env?.DEV) {
  const { autoEnable } = await import('../../../examples/mock-monitor/dev-tap.js');
  autoEnable();
}

Then open your app with ?monitor=1 (for example http://localhost:5174/?monitor=1). The monitor opens in a popup and mirrors every call. If the browser blocks the popup, click the banner the app shows.

#Drive the mock yourself

For tests, or to simulate events, install the mock explicitly from @fluffer/launcher-sdk and turn off the automatic one:

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

const mock = installMockLauncher({
  device: { id: 'test-1', family: 'Fluffer', type: 'MBBunny', state: 'Connected', motorCount: 4, hasRotation: true },
  capabilities: ['device.vibrate', 'device.rotate'],
});

const game = await createGame({ autoMock: false, capabilities: ['device.vibrate'] });

await haptics.impact(0.9);
console.log(mock.lastCall('device.setAllMotors')?.params);   // { m1: 180, m2: 180, m3: 180, m4: 180 }

mock.uninstall();
Mock optionWhat it sets
deviceThe pretend toy (DeviceSummary), or null for "no toy connected"
userThe pretend user returned by user.get()
capabilitiesWhat the mock is willing to grant (default: all)
slugAudience for mock account tokens
authPretend account for account.identity (see Accounts)
handlersPer-method overrides, e.g. make a call fail
onCallCallback for every bridge call

The handle returned by installMockLauncher() has emit(topic, data), getCalls(), lastCall(method), setHandler(method, handler), getGrantedCapabilities(), auth and uninstall().

#Inside Fluffer without a toy

If the player hasn't connected a toy, game.device is null and haptic calls do nothing, without errors. Make sure your app is still playable in that state. If a toy connects after your app has started, game-kit picks it up automatically.

#5. Build and package

npm run package
# ✓ packaged my-game → my-game.zip (0.03 MB)

npm run package builds your app, checks manifest.json with the SDK's validator, and zips dist/ plus manifest.json into <slug>.zip. That zip is what you hand to Fluffer. See Publishing for the requirements and how to submit.

Before you send it, serve the built bundle and play it end to end:

npx serve dist

#Troubleshooting

ProblemLikely causeFix
HOST_UNAVAILABLE from createGame()Running in a plain browser with autoMock: falseLeave autoMock on, or call installMockLauncher() first
PERMISSION_DENIED on a rotation calldevice.rotate isn't declared or requestedAdd it to manifest.json and to createGame({ capabilities })
Audio doesn't playBrowser autoplay rulesCreate or resume your AudioContext in the first pointerdown
Haptics keep running after game overA continuous pattern's stop function wasn't calledCall haptics.stop() and any stop functions you hold
Game keeps ticking in the backgroundUsing setInterval instead of game.onTickMove per-frame work into game.onTick
TypeScript can't find @fluffer/game-kitSDK packages not built, or app outside games/Run npm run build at the repository root and keep the app under games/<slug>/