Skip to content

Repository files navigation

Drone Simulator

Drone Simulator

Fly a racing quad or a scout helicopter over a procedural city.
Seven-mission story campaign, hostile drones, two-player battles — all in a browser tab.


Play in browser


Three.js Vite JavaScript WebGL Supabase Web Audio

Vibe coded Status Dependencies No assets


What is this?

A flight sim that behaves like a game. You take off from a command base, fly over a city of ~500 m of procedural blocks, follow a handler's voice through a story campaign, and dogfight drones that shoot back. Fuel drains, the battery sags, the radio link degrades the further out you push, and running dry drops you out of the sky.

Everything you see is generated in code. No 3D models, no textures, no sprites, no sound files. Buildings, traffic, neon, rain, explosions and every gunshot are built at runtime — which is why the whole thing has exactly one runtime dependency.

Heads up: this project was vibe-coded — built conversationally with Claude rather than planned up front. It works and it is fun to fly, but the seams show: one 5,000-line module, an assisted flight model, and client-side hit detection. Everything sketchy is listed in Known gaps. Nothing is hidden.

Quick start

git clone https://github.com/4riful/drone-simulator.git
cd drone-simulator
npm install
npm run dev          # → http://localhost:5173
Command What it does
npm run dev Vite dev server, bound to 0.0.0.0 so you can fly it from your phone
npm run preview Production-style static preview
npm run check Parses src/main.js and reports syntax errors
npm run vendor Regenerates vendor/ — see Deploying
npm run stamp Re-fingerprints the ?v= cache-busters after editing src/

Needs: Node 18+ and a WebGL2 browser. That's it.

index.html is the preflight page — mode and airframe select, no WebGL. game.html is the simulator. They're separate documents so the first paint never pays for the renderer.

Controls

Key Action Key Action
W S / Forward / back F / mouse Fire
A D Strafe left / right Tab Boost
Q E / Yaw B / Ctrl Emergency brake
Space Climb T Lock target
Shift Descend L Follow assist
R V Pitch trim C Camera distance
H Help Esc / P Pause

🎮 Gamepad — Mode 2 layout, auto-detected, with deadzone, expo, per-axis sensitivity and inversion in settings. 📱 Touch — dual sticks plus fire / boost / up / down / lock / brake.

How it flies

The controller is a per-axis target-velocity integrator, not rigid-body physics. Your stick picks a target velocity in the yaw frame and the aircraft accelerates toward it. Pitch and roll are visual — the airframe banks to sell the motion, it doesn't generate it. Yaw is the only axis with true angular velocity and damping.

It reads well in the hand. It is not a trainer. Real force/torque is planned in docs/FLIGHT_MODEL_PLAN.md.

Everything is tuned from the C block at the top of src/main.js:

Parameter Value
Max thrust 42 m/s² — about 4.3 : 1 thrust-to-weight
Top speed 48 m/s horizontal (~173 km/h) · 20 m/s vertical
Max tilt 0.78 rad (~45°)
Yaw 3.5 rad/s, accel 12, damping 5
Motor lag 40 ms input smoothing
Ground effect +20% lift below 5 m

On top of that sits a whole systems layer: wind, gusts and turbulence; air density thinning with altitude (1.0 → 0.82); fuel burn scaled by throttle and boost; battery voltage derived from fuel and load (25.2 → 18.0 V); signal strength that decays with range and low altitude; and a GPS fix that degrades 3D → 2D → NO FIX as the link gets worse. Empty the tank and you get a five-second engine-failure countdown, then an unpowered fall.

Two airframes, same controller, different multipliers:

Airframe Feel
🛩️ MQ-9 Reaper Baseline. Fast, snappy, 45° banks.
🚁 MQ-8B Fire Scout 0.82× speed, 0.65× tilt, heavier throttle response.

What's in the world

🌆 The city — 500 m grid on 84 m blocks, procedural buildings, roads, two waterways and a bridge, four named districts (Command Base, Downtown Core, Riverfront, Industrial Yard), a forward airstrip and a harbor yard, moving traffic, neon signage, rain, smoke, particles, and a time-of-day atmosphere with post-processing.

🎯 The campaignOperation Andromeda: 7 sorties across 3 acts, unlocked in sequence, driven by an objective state machine in src/story/campaign.js. Two handlers talk to you on the radio — Maj. Elena Voss and Col. Marcus Reyes — with a brief before and a debrief after every sortie. Best scores are saved per sortie. Pick Campaign on the preflight page, or Story Campaign from the in-game menu.

First Light → Ghost Signal → Cut The Bridge → Blackout Run → Hornet's Nest → The Handler → Last Light

🕹️ Free modes — Single (balanced sortie) · Training (no hostiles, 0.35× score) · Free Flight (open city, nothing shooting back) · Online Battle.

💥 Combat — 7 hostile drones, 260 m/s projectiles, 110 ms fire rate, 150 HP hull, lock-on with follow assist, rings, orbs, power-ups, explosions.

👨‍✈️ Pilots — local profiles tracking callsign, sorties, score, range, kills and flight time, plus four personas that trade score against fuel and signal:

Persona Trade
Recon Specialist +8 signal, 0.95× score, 0.96× fuel burn
Combat Pilot 1.12× score, 1.08× fuel burn
Test Pilot +2 signal, wider speed envelope, rougher air
Instructor +5 signal, 0.90× score, 0.92× fuel burn

📟 HUD — heading, speed, altitude, attitude, hull, boost, fuel, battery, signal, GPS, air density, threat count, radar/minimap, kill feed and warnings.

Online Battle

Two-player rooms over Supabase Realtime — Broadcast carries drone state and hit events, Presence tracks who's in the room. GitHub Pages can't host an authoritative server, so this is the free path.

Create a battle  →  copy the invite code (DRN-482K)  →  friend joins by link or code

Synced: callsign, persona, aircraft, position, rotation, velocity, health, fuel, hits.

⚠️ Hit validation is client-side. Anyone can edit their own damage. Making it authoritative is a to-do, not a shipped feature.

Setup lives in docs/FREE_MULTIPLAYER_SETUP.md. Optional cloud-synced battle profiles need the SQL in docs/SUPABASE_BATTLE_PROFILE_SETUP.md — without those tables, records stay local and the UI reports sync as pending.

Deploying

There is no build step. GitHub Pages serves this repo as-is, straight off main.

That only works because the bare specifiers the sources import (three, three/addons/…) — which normally need a bundler — are resolved by an import map in game.html:

{ "imports": { "three": "./vendor/three.module.min.js",
               "three/addons/": "./vendor/three/addons/" } }

vendor/ is generated, not hand-maintained. It holds the minified three build plus only the 13 addon files actually reachable from the simulator's imports — 816 KB, against 23 MB for all of examples/jsm. Regenerate it after bumping three or adding a new addon import:

npm run vendor

Assets are fingerprinted the same way — no build step means nothing does it for you. npm run stamp rewrites every local ?v= token to a hash of that file's contents, and npm run check fails if one is stale. This matters more than it sounds: with a hand-written token, shipping a fix without editing it by hand leaves every returning browser running the previous, broken file — the URL never changed, so the cache was right to keep what it had.

scripts/vendor-three.mjs walks the import graph and copies what it reaches, so a missing addon is a loud error at vendor time instead of a blank screen in production. And if the graph fails to load anyway, game.html runs a 20-second watchdog that swaps the loading overlay for a real error message and a reload button, rather than spinning forever.

Project layout

index.html              Preflight — mode + airframe select, no WebGL
game.html               Simulator shell — import map, menus, HUD, boot watchdog
│
├─ src/home.js          Preflight logic             (192 lines)
├─ src/home.css         Preflight styling
├─ src/styles.css       Terminal UI, cockpit HUD, menus, responsive
├─ src/main.js          Everything else            (5,039 lines)
│                       state · world gen · flight loop · HUD · audio
│                       input · storage · networking
├─ src/render/
│   ├─ atmosphere.js    Time-of-day sky and lighting  (435 lines)
│   └─ postfx.js        Post-processing chain         (179 lines)
└─ src/story/
    └─ campaign.js      Campaign data + MissionDirector (465 lines)

assets/logo.svg         The mark at the top of this file
check-module.mjs        Syntax check behind `npm run check`
scripts/                vendor-three.mjs — builds vendor/ for the static host
vendor/                 Generated. three + the addons the game reaches
docs/                   Flight model plan · multiplayer setup · Supabase SQL

Known gaps

Ordered by how much they'd bother you.

Gap
🟠 src/main.js is one 5,000-line module. Splitting it into state / input / world / entities / sim / UI is the next structural job.
🟠 Multiplayer hit detection is client-authoritative.
🟡 Flight is target-velocity, not rigid-body — no torque, no per-motor thrust.
🟡 Profiles are browser-local (IndexedDB, localStorage fallback).
🟡 No automated tests — npm run check only parses. Nothing catches a broken module graph but a browser.
🟡 vendor/ is committed, so a three bump in package.json silently does nothing until someone runs npm run vendor.
No LICENSE file yet.

ROADMAP.md has the phased plan.

Credits

Three.js for rendering · JetBrains Mono (OFL-1.1) · Tabler Icons (MIT) · everything visual generated procedurally. Full list in ATTRIBUTIONS.md.


Built in the browser, for the browser.

About

Browser-based drone simulator with realistic flight systems, pilot profiles, missions, and multiplayer roadmap.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages