Download

Make plugins

Plugins change FPV Sim however you like: put text and panels on the screen, add menu tabs and settings, bind keys, build things on the map, bend the physics or react to tricks, crashes and chat. They're plain JavaScript files: no compiler, no tools, just a text editor.

Needs FPV Sim 1.3.0 or newer (Windows, Android or the web version). Plugin API version: 2.

Your first plugin

Make a file called hello.js with this in it:

export const info = {
  id: 'hello',
  name: 'Hello',
  version: '1.0.0',
  description: 'Says hello and shows your speed.',
  api: 1,
};

export default function (api) {
  // Text on the screen while you fly (x / y are 0..1 of the screen).
  const label = api.ui.text('Hello pilot!', { x: 0.05, y: 0.5 });

  // Runs every frame.
  api.on('frame', () => {
    label.set(`Speed: ${(api.drone.speed * 3.6).toFixed(0)} km/h`);
  });

  api.on('crash', ({ speed }) => api.ui.popup(`Ouch! ${speed.toFixed(1)} m/s`, 'bail'));
}

In the game, open the Plugins tab → + Add plugin (.js) → pick hello.js. Press FLY: your text is on the screen.

Adding, updating and removing plugins

File format

A plugin is a JavaScript module with two exports:

ExportWhat it is
infoAn object: id (letters, numbers, . _ -; it's the plugin's identity), name, version, description, author, and api (the lowest plugin API version it needs), and keys (its shortcuts). Write it as a plain object literal so the game can read it.
defaultfunction (api) { … }: runs when the plugin starts. It can be async. It may return a function, which runs when the plugin stops.

You can import from full URLs (for example a library on a CDN), but most plugins don't need to: three.js comes with the API as api.THREE.

Good to know

Plugins can do anything the game can, including using your signed-in account on that device. Only add plugins you wrote or got from someone you trust, and read them first: they're just text.

Servers and plugins

Server owners decide whether players may use plugins on their server, in server.settings (or live with the set command):

# true: players may use plugins here. false: their plugins are paused while they're on this server.
allow-plugins=true
# Empty = any plugin. Or only these plugin ids, separated by commas:
allowed-plugins=speed-hud,lap-timer

When you join, plugins the server doesn't allow are paused (their card says Paused by server) and start again when you leave. Needs server 1.2.0 or newer; older servers allow everything.

Events

api.on(name, fn) runs fn(data) when something happens. It returns a function that stops listening.

EventWhendata
frameEvery rendered frame (menu, flight, replays…){ dt, state }: seconds since the last frame, and the game state
flightStartA flight begins (map loaded, drone on the pad){ map, drone, multiplayer }: ids, and whether it's a multiplayer round
flightEndBack to the main menunone
resetThe drone is put back on the start (R / B, auto reset, new flight)none
crashA crash or a drowning{ speed, drowned }
trickA trick is recognised{ name, points, combo }
chatA multiplayer chat line arrives (also join / leave notes){ from, text, system }
pause / resumeThe pause menu opens / closesnone
tabA main-menu tab is clicked{ tab }
arm / disarmThe motors are armed / disarmed in flightnone
join / leaveYou join a server (its lobby) / leave it or it closes{ address, server } / { reason }
playerJoin / playerLeaveAnother pilot joins / leaves the server{ id, name }
const stop = api.on('trick', (t) => api.log(t.name, t.points));
// later: stop();

api.drone

The drone you're flying. position, velocity, quaternion and rates are the live three.js objects: change them and the drone changes.

NameWhat
positionVector3, metres
velocityVector3, m/s
quaternionQuaternion, the attitude
ratesVector3, body rates in rad/s (x pitch, y yaw, z roll)
speedm/s (read only)
altitudemetres above the ground straight below (read only)
headingradians, 0 = facing −Z (read only)
armed, mode, crashedarmed or not; flight mode ('acro', 'angle' or 'horizon': set it to switch); crashed and waiting for a reset
battery{ voltage, charge, usedMah }: volts, 0..1, mAh used
specThe drone's spec: name, mass, motor, prop, battery, rates, crashSpeed… Mostly for reading. crashSpeed works right away when changed; the physics (mass, thrust) is worked out when the flight starts. Changes last until the game restarts.
arm(), disarm()Arm or disarm the motors
reset()Back to the start of the map
teleport(x, y, z, heading?)Move the drone there, stopped (keeps it armed)
// A "boost" shortcut (declared in info.keys, see below): shoot forward.
api.keys.bind('boost', () => {
  const fwd = new api.THREE.Vector3(0, 0, -1).applyQuaternion(api.drone.quaternion);
  api.drone.velocity.addScaledVector(fwd, 15);
});
// Indestructible drone.
api.drone.spec.crashSpeed = 999;
Pick keys the game doesn't use. The game uses W A S D, X, Shift, the arrows, Space, Enter, Esc, P, R, C, N, M, V and T.

api.world and 3D

NameWhat
api.THREEThe whole three.js library the game uses (meshes, materials, lights, vectors…)
world.sceneThe three.js Scene being drawn
world.map{ id, name } of the loaded map, or null in the menu
world.spawnVector3: the start point on the ground
world.add(object)Adds a three.js object to the scene. It's removed when the plugin stops.
world.remove(object)Takes it away again
world.groundDistance(x, y, z, max?)Metres straight down from that point to the first surface (Infinity if none)
const { THREE } = api;
api.on('flightStart', () => {
  const box = new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2), new THREE.MeshStandardMaterial({ color: 0x39ff6a }));
  box.position.copy(api.world.spawn).add(new THREE.Vector3(0, 3, -10));
  api.world.add(box);
});

Objects you add are drawn but the drone doesn't collide with them; use the drone's position to make your own triggers (see the Ring Race example).

api.ui: screen, menus and settings

Text on the screen

api.ui.text(content, options) returns { el, set(text), show(on), remove() }.

OptionWhat
x, yWhere its top-left corner goes, as 0..1 of the screen
when'flying' (default: in flight and pause), 'menu' or 'always'
htmltrue to treat content / set() as HTML
styleAny CSS, e.g. { fontSize: '30px', color: '#ff6a1a', transform: 'translateX(-50%)' }

Panels

api.ui.panel(title, { x, y, width, when }) gives a boxed window with a title and a hide button. Put anything in .body. It returns { el, body, show(on), remove() }. Panels take clicks; text doesn't. when defaults to 'always'.

const p = api.ui.panel('Tools', { x: 0.02, y: 0.3, width: 200, when: 'flying' });
p.body.append(
  api.ui.el('button', { textContent: 'Back to start', onclick: () => api.drone.reset() }),
);

Building elements

api.ui.el(tag, props, children) makes an HTML element. props can set any property (textContent, value, class…), style as an object, and events as onclick, onchange, oninput… You can also use normal DOM code (document.createElement).

A menu tab

api.ui.menuTab(title, render) adds a tab to the main menu. render(container) draws it every time it's opened. Returns { open(), redraw() }. Class names that match the game's look: mp-box (a panel), sub (grey text), cards wide + card (a grid of cards), fly (an orange button).

api.ui.menuTab('My Tab', (box) => {
  box.append(api.ui.el('div', { class: 'mp-box' }, [
    api.ui.el('h2', { textContent: 'Hello from a plugin' }),
    api.ui.el('button', { class: 'fly', textContent: 'Fly Old Town', onclick: () => { api.game.settings.mapId = 'town'; } }),
  ]));
});

Settings

api.ui.settingsSection(title, render) adds a section at the end of Settings. Use the field class for a label + control row like the game's own settings, and api.storage to remember choices.

Messages

ui.osd(text, kind?, seconds?)A message in the middle of the flight screen. kind: '', 'warn' or 'bad'.
ui.popup(html, kind?)A short popup like the trick points. kind: '', 'bank' (green) or 'bail' (red).
ui.notice(text)A message box with an OK button.
ui.css(text)Adds your own CSS to the page (removed with the plugin). Restyle anything in the game.

Shortcuts (api.keys)

Declare your plugin's shortcuts in info.keys. Each one gets a row in Settings → Plugin shortcuts, where the player can set it to any key or controller button (or both). Then bind it by its name:

export const info = {
  id: 'turbo', name: 'Turbo', api: 2,
  keys: {
    boost: { name: 'Boost', key: 'KeyB', pad: 3 },   // default: B, or Y on a controller
    panel: { name: 'Open panel', key: 'F4' },
  },
};

export default function (api) {
  api.keys.bind('boost', () => { /* … */ });
  api.ui.osd(`Boost: ${api.keys.label('boost')}`);  // what it's set to now, e.g. "B / 🎮 Y"
}
NameWhat
keys.bind(name, fn, when?)Runs fn when the shortcut is pressed. name is a shortcut from info.keys, or a raw key code like 'KeyG' (which the player can't change). when: 'flying' (default), 'menu', 'paused' or 'always'. Returns a function that unbinds it.
keys.label(name)What a declared shortcut is set to now, to show the player
key (in info.keys)Default key, as a key code: 'KeyG', 'Digit1', 'F2', 'Numpad5', 'PageUp'… It's the physical key, so it works with any keyboard language.
pad (in info.keys)Default controller button: 0 A, 1 B, 2 X, 3 Y, 4 LB, 5 RB, 6 LT, 7 RT, 8 Back, 9 Start, 10 / 11 stick clicks, 12–15 D-pad up, down, left, right

Shortcuts never fire while the player types in a text box (like the chat), and a key a plugin uses (F5, PageUp…) won't also reload or scroll the page. In Settings, a key the game itself uses is outlined in yellow. The game uses W A S D, X, Shift, the arrows, Space, Enter, Esc, P, R, C, N, M, V and T.

api.physics and api.time

Multipliers on the real physics: 1 is normal. If two plugins change the same value, they multiply together. Everything goes back to normal when the plugin stops.

physics.thrustLift from the props (the steering stays the same)
physics.dragAir drag on the body and props
physics.gravityGravity (1/6 = the Moon)
physics.propSpeedProps stop pushing once the air through them is as fast as their pitch speed. Above 1 raises the drone's top speed.
physics.batteryBattery drain (0 = never runs down)
physics.reset()Your plugin's values back to 1
time.scaleSpeed of the whole sim: 0.25 slow motion, 2 double speed (0.05 to 4)
// Break the top speed: about 112 → 190 km/h on a Freestyle 5".
api.physics.propSpeed = 2;
api.physics.thrust = 1.4;
api.physics.drag = 0.55;

api.input: the sticks

input.modify(fn) runs fn(sticks) every flight frame, before the flight controller reads the sticks. Change the values to fly the drone yourself: autopilots, assists, input curves, return to home…

sticks.throttle0..1
sticks.yaw, .pitch, .roll−1..1 (right, forward and right are +)
input.sticksA copy of this frame's sticks, for reading
input.isDown(code)True while that key is held
input.buttonsController buttons held right now
// Altitude hold: keep the height you had when you pressed the shortcut.
let target = null, base = 0.5;
api.keys.bind('hold', () => {
  target = target === null ? api.drone.position.y : null;
  base = api.input.sticks.throttle;
});
api.input.modify((s) => {
  if (target === null) return;
  const err = target - api.drone.position.y;
  base += err * 0.0004;
  s.throttle = base + err * 0.06 - api.drone.velocity.y * 0.05;
});

api.camera

camera.objectThe three.js camera being drawn
camera.mode'fpv', 'chase' or 'los' (line of sight)
camera.fovFPV field of view in degrees; set it to zoom
camera.uptiltFPV camera uptilt in degrees; set it to tilt
camera.cycle()Next camera

api.sound

sound.beep(freq?, ms?, volume?, wave?)A beep: Hz (880), length (120 ms), volume 0..1 (0.2), 'sine', 'square', 'sawtooth' or 'triangle'
sound.say(text)Reads the text aloud with the computer's voice

api.storage

Saved on the device, separately for each plugin, and kept across restarts. Values can be anything JSON can hold.

const best = api.storage.get('best', 0);   // second value = default
api.storage.set('best', 42.5);
api.storage.remove('best');

api.game

game.state'menu', 'loading', 'flying', 'paused', 'map' (the M map is open), 'naming' (naming a recording), 'replay' or 'lobby'
game.multiplayertrue during a multiplayer round
game.settingsThe player's settings, live (droneId, mapId, volume, fpsCap, miniMap…). After changing some, call game.applySettings() to save them and redraw Settings.
game.rawThe whole game object: the physics, flight controller, camera rig, renderer, sound and more. Everything is reachable here, but it's the game's inside, so names can change between versions; prefer the API where it has what you need.

api.multiplayer

multiplayer.connectedOn a server right now
multiplayer.myIdYour player id (0 when not on a server)
multiplayer.playersEveryone on the server: { id, name, drone, level, ping }
multiplayer.dronesWhere the other drones are: { id, position, drone }
multiplayer.say(text)Sends a chat line as you (if chat is on). Please don't spam servers.

Listen with api.on('chat', …).

Timers, logs and cleanup

api.every(ms, fn)Runs fn every ms milliseconds until the plugin stops. Returns a stop function.
api.after(ms, fn)Runs fn once after ms milliseconds (cancelled if the plugin stops)
api.log(...)console.log with your plugin's id in front
api.onUnload(fn)Runs when the plugin is turned off or removed (for things you made yourself, outside the API)
api.info, api.versionYour plugin's id, name and version; the game's plugin API version

Example plugins

These come with the game (Plugins tab → Examples) and are here to download and change.

Speed HUD

Big speed and height numbers; a Settings section for km/h or mph.

speed-hud.js
Moon Gravity

Press G to switch to 1/6 gravity. Shows a declared shortcut and api.physics.

moon-gravity.js
Ring Race

3D rings near the start, a timer, best times and its own menu tab.

rings.js
Flight Stats

A panel with crashes, tricks and top speed. H shows and hides it.

flight-stats.js

Recipes

Do something once per second

api.every(1000, () => { if (api.game.state === 'flying') api.log(api.drone.position); });

A chat command

api.on('chat', ({ from, text }) => {
  if (text === '!where') api.ui.popup(`${from} wants to know where you are`);
});

Slow motion while you hold a key

let slow = false;
api.keys.bind('KeyQ', () => (slow = !slow));
api.on('frame', ({ dt, state }) => {
  if (!slow || state !== 'flying') return;
  api.drone.velocity.multiplyScalar(1 - 2 * dt);  // heavy air
});

Restyle the game

api.ui.css(`#osd { color: #39ff6a; }  #fly { background: #2e7dff; }`);

Ideas to build

Looking for something to make? Every one of these can be done with the API above:

Autocomplete in your editor (types)

Download flysim-plugin.d.ts, put it next to your plugin, and add this line at the top of the plugin. Editors like VS Code then show every API function with its description as you type.

/** @param {import('./flysim-plugin').PluginApi} api */
export default function (api) { … }

Debugging