
GamesMultiplayer, 1–10 players
OpenStrikeRT
ThreeRTFirst-party
Playing now
Create a roomNo open rooms right now. Create one: it opens the game in ThreeRT, where you get a link to share.
About
OpenStrikeRT
A path-traced team shooter you play together: open it and you are in a match within seconds. Two teams, one compact map, three weapons, every light traced: sun through the foundry's skylights onto a polished floor, the furnace's glow, muzzle flashes that light the room for a frame or two.
It is the first first-party game of threert.app and its showcase for real-time multiplayer.
The whole game is ThreeRT game code: TypeScript against @threert/scene (the scene), @threert/gameplay (input,
the HUD, the host channel) and @threert/net (rooms). No engine changes, no native code.
- Modes: team deathmatch (Ember against Azure, the default) and free-for-all, in rounds: first to the score limit or ahead when the clock runs out.
- Weapons: the VK-5 Carbine (automatic), the M12 Breacher (a shotgun, nine pellets), the LR-9 Marksman (a scope). Hitscan with spread that opens with movement and with every shot, recoil that kicks the view and mostly comes back, damage by zone (head, body, limbs) and by range, magazines and reloads.
- Movement: run, sprint, crouch, jump, a little air control, steps and slopes walked, ladders climbed.
- The map, Foundry: two team bays and three lanes between them: an open sunlit yard with a loading dock, the dim hall with its furnace, and a gantry 3 m up along the hall with a passage under it. Built in code.
- Up to 10 players a room. Alone or offline it is practice against bots on the same map.
Controls
W A S D move · Shift sprint · C (or Ctrl) crouch · Space jump · mouse aim · left button fire · right button sights · R reload · 1 2 3 or the wheel: weapons · Tab scoreboard. Click the picture to capture the pointer; Esc lets go.
Running it
In ThreeRT (macOS, iPhone): threert dev in this directory; once it is published, threert open https://openstrike.threert.app. A private room's invitation is https://openstrike.threert.app/#room=<CODE>.
With the engine's toolchain alone (a checkout of ThreeRT with its renderer
built, beside this one or at THREERT_ENGINE):
node tools/build.ts # -> out/openstrike.js (the bundle; @threert/net from vendor/net)
<engine>/renderer/target/release/threert-render play out/openstrike.js --assets . --presets <engine>/presets \
[--launch '{"room":"new","name":"Ada"}'] [--hud-log]
That window has real sockets and captures the pointer; it draws no HUD (--hud-log prints it). How the game is
opened decides where the player goes (host.launch, the app's launch message):
launch | |
|---|---|
room: "K7Q4MZ" | join that room |
room: "new" | a private room of your own; the HUD shows its code and the invitation |
| nothing | quick play: the public room with the most players and a free place, else a new public one |
practice: true | no room: bots on the same map (bots: n, targets: true for bots that stand still) |
name, mode ("tdm" / "dm"), map (an id) | the nickname; for a room this player creates, its mode and map |
api | the platform's address (default https://threert.app; also read from the manifest's platform.rooms) |
id | the player's stable id, when the app keeps one (the same id is the same player after a reconnection) |
rules | for a room this player creates: {scoreLimit, roundTime, respawn, intermission, protect} |
No network, no platform at the address, a room that is full or gone, a room on a map this copy does not have: the player is put in practice and the HUD says why.
How it is put together
| File | What |
|---|---|
game.ts | the frame: input, the player, the shots and what they met, the camera, what is sent and shown |
src/config.ts | every number: movement, the weapon table, the match rules (and what a room is created with) |
src/player.ts | the player's capsule: fixed 120 Hz steps, shape sweeps, steps, slopes, ladders, the crouch |
src/weapons.ts | the weapons in hand: rate of fire, magazines, reloads, sights, spread, recoil |
src/match.ts, src/net.ts, src/practice.ts | the match and its two sources: a room of the platform, or bots here |
src/soldiers.ts | the other players: bodies built and posed in code, and the volumes shots are tested against |
src/viewmodel.ts, src/effects.ts | the first-person weapon; flashes, tracers, sparks, marks, casings |
src/hud.ts, src/sound.ts | the HUD's description; sound call sites |
src/maps/ | what a map is (types.ts, docs/MAPS.md), Foundry and Range in code, mounting any map |
maps/index.ts | the imported maps this copy bundles (empty as shipped) |
tools/ | the build, the weapon models (weapons/), textures (assets.ts), pictures (shots.ts), the tests |
Netcode, on a page
A room is one Durable Object of the platform (@threert/net, platform docs § 7) running the fps rules module:
the room is the authority on the match, the clients are the authority on what they did.
What a client decides (and the room has to take on trust, within checks):
- Its own movement. The client simulates its player and sends a pose 20 times a second: position (the feet), yaw, pitch, speed, flags (crouched, firing, in the air, sights, reloading, sprinting), the weapon. The room relays poses and keeps a second of everyone's positions; it does not simulate movement and has no map.
- What its shots hit. Hit detection runs on the shooter's client, against the map's collision and against the other players' hit volumes (a head sphere, a torso capsule, limb capsules from the same skeleton that is drawn) placed from the interpolated poses the shooter sees, 100 ms in the past. What you see is what you hit: no leading for lag.
What the client sends: shot {weapon, shotTime, o, e} for every shot (the others draw its flash and tracers
from it; the room counts it against the magazine and the rate of fire), hit {target, weapon, zone, at, shotTime}
for every pellet that landed, reload {weapon, time}, die {cause} for a fall out of the map.
What the room decides: whether each claim was possible, and everything that follows. A hit is taken when the
round is live, the weapon exists and could fire then (the table's rate of fire, magazine and reload), the shooter was
alive and within the weapon's range of the point, and the target's body was at the point when the shooter saw it
there (the room rewinds by the interpolation delay, with a tolerance of 100 ms and 0.75 m). The room then computes
the damage from its copy of the weapon table (zone multiplier × range falloff), and owns health, deaths, respawn
times, team assignment (the smaller team), the score, the round clock, who won, and the next round. All of it comes
back as shared state (players.<pid>, score, round) and events (damage, kill, respawn, round, reject)
and is shown as it comes: the HUD never predicts a kill.
One table. src/config.ts holds the weapons once. roomRules() puts their damage, zones, range, falloff,
rate, magazine, reload time and pellets into the room's meta.rules when a room is created; the client's side of
the same table (spread, recoil, handling) stays here. A client that joins reads the room's numbers back
(state.rules), so a room made by another version of the game is played by its rules.
Clocks. Shot times are in the room's clock (room.time, kept from pings). Shots are timed inside a frame, so
the rate of fire is the table's at any frame rate and the room's rate check passes without slack being used up.
Connections. A dropped connection reconnects for 30 s as the same player (same id, same pid, same team and score). After that, or when the room says no (full, gone), the player is in practice and told why. There is no host: authority is the room's, so nobody's leaving moves it.
Cheating surface, honestly
This is a client-authoritative shooter with a referee, not a server-authoritative one. A modified client can:
- Move however it likes (speed, flight, through walls): the room does not check movement at all.
- Claim hits it did not earn, as long as each is plausible: an aimbot is indistinguishable from good aim, and a claim through a wall is taken, because the room has no map. What the room does stop: firing faster than the weapon, more pellets than a shot has, shooting while dead or reloading (as the room counts it), hitting from out of range, hitting a player who was not near the claimed point, damage other than the table's.
- See everyone (poses of all players reach every client).
- Not report being hit: it cannot, damage is the room's.
Private rooms among friends are the intended use for anything that matters. Movement checks against the map in the room, and server-side ray tests, are what a competitive mode would need; neither exists.
Known limits
- Players do not collide with each other, and team-mates do not block bullets.
- The magazine of a weapon survives death: the room keeps each player's guns for the whole round, so the client reloads what is not full the moment it dies and the room counts that reload while the player waits to respawn. In free-for-all (2.5 s respawn) a slow reload can still be running for a fraction of a second after the respawn.
- Spawn protection (1.5 s, ended by firing) is known for the player only: others are not shown as protected, and
hits on them in that time are refused by the room (
reject: protected). - A fall does no partial damage (the room has no "hurt yourself" event, only
die). - Remote soldiers are posed in code from a pose's few numbers: no ragdolls, no reload or jump animation beyond a tucked pose.
- The HUD is drawn by the app, from the engine's schema: a name over a player is the name under the crosshair, not
a tag in the world. The engine's own window (
threert-render play) draws no HUD at all. - No sound yet: the runtime has no audio API. See below.
- Bots (practice only) wander and shoot; they do not path-find, use cover or climb.
- One built-in competitive map. Imported maps:
docs/MAPS.md.
Sound
src/sound.ts: every place a sound belongs calls sound(name, position?), which posts {t: "audio", name, at?, gain?} on the host channel: a message family no app implements yet (an app ignores messages it does not know). A
client that adds audio maps the names (weapon.carbine.fire, weapon.<id>.fire.far, weapon.reload,
impact.wall, step, land, hurt, death, hit, kill, respawn, round.start, …) to samples or synthesis,
answers {t: "audio.ready"} once, and is then also sent the listener ({t: "audio.listener", at, yaw}, six times a
second). Nothing in the game's logic changes.
Tests
All of them run the real game in the engine's headless renderer with a script as the app (tools/lib/):
node tools/test-move.ts # movement on numbers, at 30 / 60 / 120 Hz (the Range map)
node tools/test-weapons.ts # rate of fire, recoil, bloom, reloads, the sights, damage by zone, at 30 / 60 / 120 Hz
node tools/test-match.ts # two players in a real room: hits, deaths, respawns, the score, a round won; the ways in
node tools/test-maps.ts # an imported map in practice and in a room
<engine>/node_modules/.bin/tsc -p .
test-match and the room part of test-maps need the platform's rooms on this machine (in the platform
repository: npx wrangler dev --port 8933 --persist-to /tmp/os-rooms-state --var DEV_ORIGIN:http://127.0.0.1:8933).
node tools/shots.ts takes the pictures in media/.
Assets
Everything shipped is original or CC0: assets/CREDITS.md. The weapons are modelled in code
(node tools/weapons/build.ts), the textures are Poly Haven's (node tools/assets.ts copies them from the machine's
asset store). vendor/net/ is @threert/net, synced from the platform repository by node tools/sync-net.ts
(vendor/net/SOURCE.json says from which commit); the ThreeRT CLI resolves the package itself when publishing.
Controls
W A S D move · Shift sprint · C crouch · Space jump · mouse aim, left fire, right sights · R reload · 1 2 3 or wheel: weapons · Tab scores
Remix
This package carries its source. The command makes a project from it on your Mac, with a prompt for your coding agent; Remix in ThreeRT for macOS does the same.
threert remix https://openstrike.threert.app/Versions
1 published- 0.1.0current








