A cloud-persistent web multiplayer 2d game where players build spaceships.
You start as a single cockpit block floating in the world. Use the ship editor to build your spaceship, then fly it around and interact with the world.
| Input | Action |
|---|---|
W / Arrow Up |
Thrust forward |
S / Arrow Down |
Thrust backward |
A / Arrow Left |
Turn left |
D / Arrow Right |
Turn right |
Space |
Pause / unpause |
X |
Trigger explosion at cursor position |
| Left-click + hold | Attract nearby entities toward cursor |
| Right-click (empty space) | Spawn a new entity |
| Right-click (entity) | Delete that entity |
Ctrl + Left-click |
Take control of a nearby entity |
| Input | Action |
|---|---|
| Joystick (bottom-left) | Up/down: thrust — Left/right: turn |
| Tap (normal mode) | Attract entities toward tap point |
| Tap (right-click mode) | Spawn or delete entity at tap point |
| Double-tap | Take control of tapped entity |
| Right-click mode button | Toggle between attract and spawn/delete tap behavior |
- Ship building — Open the editor to place and remove blocks on a 33×33 tile grid. Your ship's physics (mass, moment of inertia) update automatically when you save.
- Physics simulation — Ships have realistic linear and angular momentum. Collisions exchange impulses between entities, applying both linear and rotational forces.
- Explosions — Press
Xon PC to trigger an explosion at your cursor. Explosions cast 360 rays that damage and push blocks; destroyed blocks allow rays to continue through. - Entity splitting — If an explosion or collision disconnects parts of a ship, those parts split into separate independent entities.
- Attraction — Hold left-click (or tap on mobile) to pull nearby entities toward your cursor with a distance-scaled force.
- Multiple entities — Spawn extra ships with right-click. Switch control between them with
Ctrl+click (PC) or double-tap (mobile). - Particle effects — Blocks emit colored debris particles when destroyed. Particles are ephemeral (not saved) and fade out over their lifetime.
- Cloud saves — Game state is saved to Google Drive automatically every 30 seconds and on page close. Your ships persist across sessions.
The game runs entirely in the browser as a single-page application served by a Python/Flask backend. There is no server-side game simulation — all physics run client-side in JavaScript.
| File | Responsibility |
|---|---|
static/js/main.js |
Game loop, input handling, camera, explosions, entity lifecycle |
static/js/tiles.js |
Block registry, tile rendering, collision detection, physics helpers |
static/js/editor.js |
Ship editor overlay (canvas-based block placement UI) |
static/js/drive.js |
Google Drive OAuth 2.0 integration, save/load, autosave |
| Parameter | Value |
|---|---|
| World size | 1000 × 1000 px (testing) |
| Tile size | 16 px |
| World boundary | Hard walls with 16 px margin; velocity clamped on impact |
| Parameter | Value |
|---|---|
| Thrust | 0.6 force/frame |
| Turn speed | 0.025 rad/frame |
| Angular damping | 1.0 (no drag) |
| Restitution (bounciness) | 0.35 |
| Baumgarte position correction | 40% overlap per step, 0.5 px slop |
| Sub-steps | 1–8 per frame, scaled so displacement per step ≤ 8 px |
| Base entity mass | 1.0 + sum of block masses |
The physics pipeline each frame:
- Force application — player input and attraction forces update velocities.
- Sub-stepped integration — positions and angles integrated in up to 8 sub-steps.
- Collision resolution — SAT (Separating Axis Theorem) narrow phase finds deepest contact; impulse resolution handles both linear and angular response via the parallel axis theorem.
- Entity cleanup — entities with no blocks are removed; disconnected block graphs are split into new entities.
- Explosion impulse flush — deferred explosion impulses are distributed to entities (or proportionally to split pieces by proximity).
The camera uses lagged lerping in all three axes to smoothly follow the player. Position lerping is performed in camera-local space to eliminate circular drift when angle is also lagging.
| Parameter | Value |
|---|---|
| Position lag | 8% per frame |
| Rotation lag | 10% per frame |
| Zoom lag | 6% per frame |
| Minimum visible height | 40 tiles (640 px world-space) |
Zoom automatically scales so the player's interaction radius fills the viewport height.
Explosions use a raycasting model:
- 360 rays cast from the explosion origin.
- Each ray carries
strength / 360of the total strength budget. - Rays attenuate linearly with distance (range = √strength px).
- Blocks are damaged; if a block's health is less than the current ray strength, it is destroyed and the ray continues with reduced strength.
- Impulses are deferred and applied after entity splitting, so split pieces each receive physically correct forces.
| Parameter | Value |
|---|---|
| Default strength | 5000 |
| Rays | 360 |
| Expansion speed | 0.5 px/ms |
| Impulse scale | 0.1 per unit ray strength |
Block types are defined server-side and served from /api/block-registry, cached in localStorage. Each block type has:
size— tile footprint (x × y tiles)color— RGB display color (fallback when noshapesstring is defined)maxHealth— hit pointsmass— contribution to entity massshapes— optional shape descriptor string (see below)
Each block instance on an entity stores its current health. Damage reduces health; at zero the block is removed, and the entity's physics properties are recomputed. If the remaining blocks are no longer all connected, the entity is split.
The editor's ↻ Reload Blocks button force-fetches a fresh registry from the server, discarding the localStorage cache. Useful during development when block definitions change.
The shapes field is a comma-separated list of shape commands. Each shape is a colon-separated token string:
r:x:y:w:h:rot:cr:cg:cb[:ca] filled rectangle
c:cx:cy:radius:rot:cr:cg:cb[:ca] filled circle
All position/size values are in tile units relative to the block's top-left corner. Color channels (cr, cg, cb, ca) are in the 0–1 range. rot is 0–1 where 0 = 0°, 0.5 = 180°, 1 = 360°; values outside this range are modulo'd. Alpha (ca) is optional and defaults to 1.
Any value can be a math expression instead of a plain number:
| Variable | Meaning |
|---|---|
t |
Seconds of game-time elapsed (freezes when paused) |
h |
Block health ratio (1 = full health, 0 = destroyed) |
x |
Tile-unit X position of the current pixel within the block (color fields only) |
y |
Tile-unit Y position of the current pixel within the block (color fields only) |
Available functions/constants: sin cos tan abs sqrt pow log floor ceil round min max PI E
Available operators: + - * / % ** (exponent)
x and y in position or size fields always resolve to 0. Using x, y, t, or h in color fields triggers a per-pixel slow path (JS pixel loop via OffscreenCanvas). This has a significant performance cost — use sparingly, especially on large blocks or at high DPI.
Examples:
r:0:0:1:1:0:0.5*h:0:0 rectangle that fades from red to black as health drops
r:0:0:1:1:0:x:y:0 gradient: red increases left→right, green top→bottom
r:0:0:1:1:0:0.5+0.5*sin(t):0:0 rectangle that pulses red over time
Currently, cockpits (their rectangular base) rotate when taking damage:
r:0:0:1:1:0.05*(1-h):0.24:0.63:0.78,c:0.5:0.5:0.3:0:0.6:0.88:1.0
Particles are lightweight world-space objects rendered on the game canvas alongside blocks. They are not saved to the cloud.
spawnParticle(x, y, vx, vy, lifetime, shape)| Parameter | Type | Description |
|---|---|---|
x, y |
number | World-space spawn position (px) |
vx, vy |
number | Initial velocity (px/frame, same units as entities) |
lifetime |
number | How long the particle lives (seconds); countdown pauses when the game is paused |
shape |
string | Shape descriptor string — same format as block shapes. h resolves to the remaining lifetime fraction (1 = just spawned, 0 = about to expire) |
When a block is destroyed (health reaches 0), 4 debris particles spawn at the block's center with random directions and speeds of 0.5–2 px/frame, each living for 1 second. Their shape is a filled circle with radius ¼ tile, colored to match the first (bottom-most) shape of the destroyed block, and with alpha = h so they fade out as they age.
Game state is serialized to JSON and stored in Google Drive under /.webspace/save.json. The save includes all entity positions, velocities, angles, and complete block data for every entity. Autosave triggers every 30 seconds and on page hide/close.