Instance
An instance is one running match of Quake 0 — one level, up to five fighters, and all the bullets in flight. This folder holds the instance's constants (the level itself and every physics number) and the authoritative update that runs the whole game each tick.
gamehoster-instance-schema.json
Everything here is constant for the life of the instance and sent once to every viewer, so
the front end can size and draw the level exactly as the server simulates it. The
level is a 30×12 string, but it is not fixed here — it is chosen per instance in
instance-create (see below). The rest are
physics numbers: gravity, jumpV (tuned so a jump rises about 1.5
tiles — just onto the next ledge up), runSpeed, maxFall (capped below
a tile per tick so a fall can never tunnel through a floor), the playerW/
playerH hit-box, bulletSpeed, fireEvery (5 ticks — twice
as fast as Asteroids), the knockX/knockY hit impulse (a hit always
launches you a little upward), fireBlock (60 ticks ≈ 2 s of muted fire after
a hit), deathBelow (50 tiles under the level), and the bullet-grid padding.
gamehoster-instance-create.js
Runs once when an instance is made, and does two things. First it picks this instance's
level: five variants are laid out right in the code as arrays of strings, one
row per line so each reads exactly like the platforms it makes — a floor cell is an
_ and everything else is empty air. One is chosen at random and stored in the sent
level state. A row shorter than the width is padded as empty, and anything past the
width is ignored, so the maps are easy to edit without counting spaces.
const LEVELS = [
[
" ",
" ___ ___ ",
" ___ ___ ",
" ______ ",
" ____ ___ ",
" ___ ____ __ ",
" ___ __ __ ",
...
"____ ____ ___ ____ "
],
... four more ...
]
const pick = LEVELS[Math.floor(Gamehoster.Entropy() * LEVELS.length)]
Second, it builds the one piece of server-only persistent state the update keeps: the bullet grid — one bucket per tile, extended ten tiles past every edge (bullets can fly off the floating level while a knocked-off player is still being shot at). Each tick the update clears and re-fills it, so a fighter's hit test only looks at the handful of buckets its body overlaps.
gamehoster-instance-update.js
The authoritative per-tick pass. The whole game runs here, in order, so nothing races:
- Move each fighter. Horizontal motion is direct — run-speed × your held direction, changeable in mid-air. Vertical motion is a velocity under gravity, kicked up by a jump and zeroed on landing. Platforms are one-way: only while descending do we test the two bottom corners against every floor line the bottom edge crossed this tick, and stop at the first solid one. Jumping up, you pass straight through. Pressing down punches you just below the current floor so you fall to the next one — once per press.
- Separate any overlap. No two fighters are ever left in exactly the same place: a pair whose centres are too close is nudged apart, so a point-blank shot always has somewhere to go and can connect.
- Move the bullets in straight lines, turn any that cross a floor into a hit effect, drop any that leave the padded grid, and file the rest into the bullet grid.
- Test bodies. For each fighter, check only the nearby buckets with an exact segment-vs-rectangle test. A hit flings them the way the bullet was travelling (so a shot never flips you back on itself), locks their facing, mutes their fire for two seconds and takes their control until they land.
- Drop the fallen. Anyone more than fifty tiles below the level counts one death, scores a point for everyone else, and respawns above the level to fall back in.
- Fire. Each fighter, on its own phase, sends a bullet at the nearest rival — or, at point-blank range, in a random direction, which still lands because they overlap.