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.
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
- Add: Plugins tab → + Add plugin (.js). You can pick several files at once. A plugin starts as soon as it's added.
- Update: add the file again. A plugin with the same
idreplaces the old one. - Turn off / on: the button on its card. Everything it added through the API (text, panels, tabs, settings, keys, 3D objects, timers) is removed when it's off.
- Reload all restarts every plugin that's on.
- Save .js downloads a plugin's file (handy for the examples).
- Plugins are saved in the game on that device and start again every time the game opens.
File format
A plugin is a JavaScript module with two exports:
| Export | What it is |
|---|---|
info | An 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. |
default | function (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
- Distances are in metres, speeds in m/s, angles in radians, time in seconds.
- The world uses three.js axes: +Y is up, the ground is the X/Z plane, and heading 0 faces −Z ("north", up on the map).
- A plugin that throws errors over and over (say every frame) is switched off automatically, and the error shows on its card.
- Plugins run on your device only. In multiplayer, other pilots don't get your plugins, and the server still decides things like who's on it and the chat.
- Flight Path XP is checked by the server (daily and per-minute limits), so a plugin can't level you up faster than flying can.
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.
| Event | When | data |
|---|---|---|
frame | Every rendered frame (menu, flight, replays…) | { dt, state }: seconds since the last frame, and the game state |
flightStart | A flight begins (map loaded, drone on the pad) | { map, drone, multiplayer }: ids, and whether it's a multiplayer round |
flightEnd | Back to the main menu | none |
reset | The drone is put back on the start (R / B, auto reset, new flight) | none |
crash | A crash or a drowning | { speed, drowned } |
trick | A trick is recognised | { name, points, combo } |
chat | A multiplayer chat line arrives (also join / leave notes) | { from, text, system } |
pause / resume | The pause menu opens / closes | none |
tab | A main-menu tab is clicked | { tab } |
arm / disarm | The motors are armed / disarmed in flight | none |
join / leave | You join a server (its lobby) / leave it or it closes | { address, server } / { reason } |
playerJoin / playerLeave | Another 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.
| Name | What |
|---|---|
position | Vector3, metres |
velocity | Vector3, m/s |
quaternion | Quaternion, the attitude |
rates | Vector3, body rates in rad/s (x pitch, y yaw, z roll) |
speed | m/s (read only) |
altitude | metres above the ground straight below (read only) |
heading | radians, 0 = facing −Z (read only) |
armed, mode, crashed | armed 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 |
spec | The 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;
api.world and 3D
| Name | What |
|---|---|
api.THREE | The whole three.js library the game uses (meshes, materials, lights, vectors…) |
world.scene | The three.js Scene being drawn |
world.map | { id, name } of the loaded map, or null in the menu |
world.spawn | Vector3: 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() }.
| Option | What |
|---|---|
x, y | Where its top-left corner goes, as 0..1 of the screen |
when | 'flying' (default: in flight and pause), 'menu' or 'always' |
html | true to treat content / set() as HTML |
style | Any 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"
}
| Name | What |
|---|---|
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.thrust | Lift from the props (the steering stays the same) |
physics.drag | Air drag on the body and props |
physics.gravity | Gravity (1/6 = the Moon) |
physics.propSpeed | Props stop pushing once the air through them is as fast as their pitch speed. Above 1 raises the drone's top speed. |
physics.battery | Battery drain (0 = never runs down) |
physics.reset() | Your plugin's values back to 1 |
time.scale | Speed 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.throttle | 0..1 |
sticks.yaw, .pitch, .roll | −1..1 (right, forward and right are +) |
input.sticks | A copy of this frame's sticks, for reading |
input.isDown(code) | True while that key is held |
input.buttons | Controller 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.object | The three.js camera being drawn |
camera.mode | 'fpv', 'chase' or 'los' (line of sight) |
camera.fov | FPV field of view in degrees; set it to zoom |
camera.uptilt | FPV 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.multiplayer | true during a multiplayer round |
game.settings | The player's settings, live (droneId, mapId, volume, fpsCap, miniMap…). After changing some, call game.applySettings() to save them and redraw Settings. |
game.raw | The 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.connected | On a server right now |
multiplayer.myId | Your player id (0 when not on a server) |
multiplayer.players | Everyone on the server: { id, name, drone, level, ping } |
multiplayer.drones | Where 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.version | Your 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.
Big speed and height numbers; a Settings section for km/h or mph.
speed-hud.jsPress G to switch to 1/6 gravity. Shows a declared shortcut and api.physics.
moon-gravity.js3D rings near the start, a timer, best times and its own menu tab.
rings.jsA panel with crashes, tricks and top speed. H shows and hides it.
flight-stats.jsRecipes
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:
- Autopilots: altitude hold, position hold, GPS rescue (fly home and land), a waypoint mission, "follow that drone" in multiplayer.
- Game modes: ring races, a slalom, tag in multiplayer, a precision-landing challenge, deliveries between two spots, a crash-free streak score.
- Training: a hover trainer that scores how still you hold, a stick display, a throttle-control meter, a power-loop coach that checks your height.
- Physics toys: Moon or Jupiter gravity, slow motion, a turbo button, a heavy-payload mode, infinite battery, a speed unlocker.
- Tools: a ghost of your best run, lap timers on any map, a speed trap, a flight logger that saves CSV, a drone trail, waypoints and teleports.
- Look and sound: new HUD styles, a sports-caster voice for tricks, new colours for the whole game with
ui.css.
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
- If a plugin fails to start, or keeps throwing errors, the message shows on its card in the Plugins tab.
- Windows app: press Ctrl+Shift+F12 to open the developer tools. The Console shows
api.logoutput and full errors with line numbers. - Web version: F12 in your browser.
- Edit your file, add it again (same
idreplaces it), and it restarts with the changes.