尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Fast first-person duels in colourful low-poly block arenas (Three.js / WebGL). Inspired by the quick, competitive feel of Roblox-style arena shooters. The code, characters, weapons, UI and name are original; the weapon numbers follow Roblox RIVALS, and the Arena map's layout is converted from the public-domain (CC0) "Rivals Arena Map" 3D-print model on Printables (a copy of a layout from the Roblox game RIVALS, so keep that in mind before publishing the game widely). The other five maps and the 3D lobby are original designs made in Blender for this game.
Movement and positioning beat aim: the two guns are hitscan with zero recoil, zero spread, zero bullet travel time, no bullet drop and no camera shake. Slide, bunny hop, air strafe, wall run, double jump and grenade-boost to out-play the AI.
No installation and no build step (Three.js r159 is bundled in vendor/). The only feature that needs a server is
Online 1v1; everything else works offline.
Easiest: open index.html in Chrome (double-click it, or in ChromeOS open it from the Files app, or drag it
onto a Chrome window). That's it.
Optional – tiny local server (only if your setup blocks file:// pages):
# in this folder
python -m http.server 8000 # then open http://localhost:8000
# or: npx serve .
Pick a mode, pick a map, press Start. The mouse locks to the game (Pointer Lock). Esc releases it and pauses. In an online match the match keeps running behind the menu; Esc again (or Resume) closes it - Chrome refuses a mouse capture right after Esc, so Resume asks again for a moment instead of doing nothing.
Every Duel / Online round starts with a 3 second countdown with both fighters locked in their own spawn. After that, winning or losing a Duel or Deathmatch never pauses the game or frees your mouse, and you are never frozen: after beating the opponent you keep full control (move, jump, slide, switch weapons) while a small summary card shows and the next round / match starts by itself. (Prefer the old results screen? Settings → Mouse & ADS → Results screen after matches.)
Shift slides, and so does C. Ctrl+W closes a tab and Ctrl+1…8 switches
browser tabs (Chrome reserves those; no web page can block them), so the game needs no Ctrl: the mouse wheel, F (quick melee)
and G (quick grenade) act without it, and Settings → Graphics → Fullscreen additionally asks Chrome to deliver those combos to the game.F11).| Mode | What you do |
|---|---|
| Online 1v1 | A real match against another player. Press Online 1v1, wait in the queue, and you are paired with whoever else is searching. First to 5 rounds, 150 health each, every round starts with a 3 second countdown during which both players are locked in their own spawn. See Online 1v1 below. |
| Free-for-all (online) | An open server that is always running. Pick a map and you are in: anyone can join or leave at any time, up to 12 players per room, everybody against everybody, no rounds. A death puts you back on a spawn after 2.5 s (with 2.5 s of spawn protection); the first to 25 eliminations wins the round of the room and the scores start over. Needs the game server, like Online 1v1. See Free-for-all below. |
| Duel | 1v1 against an AI. Each round ends on an elimination, then everything resets automatically (sides alternate). First to 3 / 5 / 7 round wins. 75 s round limit: most health wins. Maps: Arena, Bridge, Crossroads, Docks, Station, Backrooms. |
| Deathmatch | Free-for-all against 1–7 AI opponents (easy / normal / hard). First to 10 / 15 / 25 eliminations within 5 minutes. ~1 s respawns, regenerating health. |
| Time Trial | Race through glowing checkpoint rings on the Gauntlet. Splits compare with your best; falling respawns you at the last checkpoint (Q). |
| Practice | Free play on every map with target dummies (also from the lobby's practice range). The Yard has a shooting range, slide hills, wall-run fields, launch pads, a bunny-hop lane and stairs. E sets a spawn point, Q returns to it, T resets. |
Everyone — you and every bot — has 150 health and no shield. Opponents' weapons are never announced: not in the round-start text, not on the death screen, not in the kill feed.
Personal bests, win streaks, accuracy, top speeds and eliminations are saved in your browser (Records).
| # | Map | Size | Idea |
|---|---|---|---|
| 1 | Arena | compact (28 x 44 m, 10 m walls) | a walled rectangle of white panelled concrete in the style of the RIVALS Arena: a long ramp up to a high ledge on each side wall (the pocket under each ledge is a covered corridor), a catwalk at ledge height bridging the middle over a low plinth, turned slabs and a tall bar on the diagonal, low blocks, and a hollowed gate (arch + windows, blue / red) that hides each team's spawn pad from the middle. A second ramp at each spawn end leads onto the ledge, and each team has six spawn points that rotate between rounds. 180-degree symmetric, so neither spawn has an advantage |
| 2 | Bridge | medium (80 x 60 m) | a city crossing: a raised bridge deck (ramps at both ends, stairs in the middle) over an avenue and a street, a yellow bus stopped on the bridge as the landmark, parked cars, jersey barriers, planters, buildings with windows around. Routes: avenue, sidewalks, under the bridge, over it |
| 3 | Crossroads | large (92 x 92 m) | open air: two roads cross at a stepped stone monument (jumpable tiers, an obelisk); a stone fort and a warehouse with windows, doors, interiors and ramp / stairs to their roofs (rotated twins on the other side), rock outcrops at the road ends, hay, walls, crates, wells and trees |
| 4 | Docks | large (96 x 76 m) | waterfront cargo yard: container alleys (single and double stacks), a gantry crane, a dockmaster's hut, a harbour office and a warehouse (two floors, windows, stairs, interiors). The north side and the piers are open water: falling in is lethal (the kill height sits 3 m under the surface) |
| 5 | Station | large (78 x 60 m) | a stylised Western railway station: two steam trains (locomotive + three carriages each, with door and window openings: fight through them) either side of an island platform with a canopy; the station hall and the depot (two floors, a stair, a balcony over an open double-height hall) |
| 6 | Backrooms | medium (56 x 56 m, 3.4 m ceiling) | an enclosed maze of 4 m yellow-office corridors, two red-carpet rooms (spawns), two pool-tile rooms and a lit atrium with a 1.2 m platform on ramps and a central column. Bright ceiling lights and a short yellow haze, so it is never too dark |
Every duel map is symmetric (Arena / Bridge / Crossroads / Station / Backrooms / Dark Arena: 180-degree rotation, Docks: left-right mirror), so
neither spawn has an advantage. The four earlier maps (Toybox, Skybridge, Pinwheel, Vertigo) were retired from the pool; they are
kept in _archive/maps-legacy.js (how to bring them back is in that file's header) and the git branch
backup/pre-rivals-overhaul holds the complete game as it was before the change.
About "Arena" and what is assumed. RIVALS' Arena is described publicly as a "clean grid design" with angled blocks and cover, a
ramp up to a centre platform, a tall wall and a hollowed-out wall, mirrored with identical spawns (the RIVALS wiki lists no
dimensions). The Arena here is modelled from scratch in Blender (tools/blender/build_testarena.py) from those descriptions and
reference screenshots: the layout, sizes and details are ours (nothing is copied from the game or from the CC0 3D-print replica
that an earlier version of this project used; that version is kept in _archive/arena-stl/), and no official dimensions are claimed.
Bridge, Crossroads, Docks, Station and Backrooms are original designs (they only take their theme and name from the RIVALS
map list: a bus on a bridge, a crossroads, docks with water, a Western station, an office maze); no layout or asset was copied.
The first time you start the game it asks for a username (3–16 characters: letters, numbers, spaces, _ and -).
It is shown to opponents in Online 1v1, on the score row and in the kill feed. Change it any time from the main menu
(Change name). Names received from other players are filtered the same way before they are displayed.
server/, no dependencies). Locally:
cd server && SERVE_STATIC=1 node server.js, then open http://localhost:8787/ in two browser windows. For a public
game deploy the server and set its address in js/config.js (SERVER_URL; it is now wss://server.sciencerevisions.online/ws, and that server's ALLOWED_ORIGINS must list the address the game is served from). Everything else (Duel, Deathmatch, Time
Trial, Practice) works without it. If the server cannot be reached the game says so; it never fakes an opponent.
Setup, trade-offs (serverless vs persistent server) and deployment steps: docs/ONLINE_BACKEND.md.startVote /
onVote / finishVote / cancelVote in server/lobby.js, settings MAP_VOTE=0 (off: the server picks a random map, as before), VOTE_OPTIONS (3), VOTE_SECONDS (12);
client: UI.showVote and the vote handlers in ; tests: .FFA_MAPS, FFA_MAX_PLAYERS = 12); a full room says so and you pick another map. Leaving is the pause menu's Leave the free-for-all,
closing the tab, or losing the connection (the game then joins the room again by itself as a fresh life).server/ffa.js (it reuses the 1v1 room's movement, lag-compensation and visibility code); client: the FFA section of js/online.js; tests: server/test/ffa.test.js.The four weapons are modelled in Blender in the Roblox RIVALS look (chunky, flat-shaded, saturated; the shapes follow RIVALS' own
default models, the real-world bases are the ones the RIVALS wiki names): the Assault Rifle is the RIVALS rifle itself, imported from a Roblox Studio OBJ export
(_models/assault-rifle/AssualtRifle.obj, converted by tools/blender/build_rifle.py; the first procedural version is in
_archive/build_rifle_procedural.py): an orange SCAR-style rifle with a navy frame, vent slots, a ribbed black magazine and two flip-up
ring sights. Its sight is one circle: you aim through the octagonal rear ring and see the front ring and its spike standing inside it; the
magazine and the charging handle (the short stick that sticks out of the left side of the handguard) are animated parts (the handle slides back on every shot); the Handgun is the pistol from _models/handgun/Handgun.obj (a Roblox export, imported by
tools/blender/build_pistol.py; the first blocky version is in _archive/build_pistol_blocky.py): a navy frame and big grip, a slate slide with three vents, a black barrel and a U-notch rear sight with the front wedge standing in it; the slide and the magazine are animated parts; the Grenade is a faceted dark-green M67 with a gold
collar and a navy fuze and lever; the Fists are two thick, short block arms (black sleeve, skin-coloured hand end). They are exported as
data (js/models-weapons.js). Animated pieces (magazine, slide, charging handle, grenade lever and pin) are separate
parts with pivots, so every animation works on them. Opponents hold simpler low-poly versions of the weapons (drawn small and
far away).
Everything lives in _models/dashduel-models.blend (one Blender scene per map: DASHDUEL_Lobby, _Arena, _Bridge, _Crossroads,
_Docks, _Station, _Backrooms, _DarkArena, _Shared (the reusable pieces), _Weapons). Each scene has a top-level collection named after the
map (BRIDGE, DOCKS, ...) with one sub-collection per kind of object (Buildings, Cover, Cars, Containers, Signs...), a
<MAP>_COLLISION proxy collection (wire boxes) and a <MAP>_PREVIEW collection (sun + cameras for inspection). All objects sit
at the origin with identity transforms, materials are DD_<map>_<name>, units are metres at game scale.
One script per map describes it as boxes / ramps / octagonal prisms (tools/blender/build_bridge.py, build_crossroads.py,
build_docks.py, build_station.py, build_backrooms.py, build_darkarena.py (reads the OBJ in _models/dark-arena/), build_testarena.py, build_lobby.py, shared helpers in mapkit.py) and, run inside
Blender, writes both halves of the map from the same scene:
| file | what | loaded by |
|---|---|---|
js/mapdata-<id>.js | collision boxes / ramps + spawns (a few KB) | the browser and the server |
js/mesh-<id>.js | the triangles of the picture, read back from the Blender objects (130-850 KB; fetched in the background while you are in the lobby, or on demand when a match starts) | the browser |
_models/exports/<id>.glb | the same geometry as glTF binary, for other tools / engines (the game itself needs no glTF loader) | nobody at runtime |
So what you see is what you collide with: there is no hand-copied collision to drift. Hidden faces (box sides inside or touching another box) are not exported. Window and door openings are real holes (you can shoot and walk through them).
build_<map>.py > Run). Each
script ends by exporting its two JS files. export_glb.py writes the GLBs, preview.py renders multi-view Workbench previews.build_*.py, change the layout, run it, then add one fromData('<id>', ...) entry (name, palette, sky,
kill height, nav step) in js/maps.js, a <script src="js/mapdata-<id>.js"> tag in index.html and the id to MAPS in
server/config.js. A map whose data file is missing is simply not offered.build_rifle.py (reads the OBJ) ... build_grenade.py, then export_models.py (-> js/models-weapons.js). The Arena is a normal map script, build_testarena.py (its id is testarena: arena is already the name of a game mode).water: {y, color} in the map entry; Docks) and can be indoor (sky.indoor: no sun, hills or clouds;
Backrooms). Runnable walls (wall flag in the data) are supported but unused by the pool.The hip pose follows the RIVALS first-person view (reference screenshots, solved numerically in tools/solve-hip-pose.js): the gun sits low
on the right, large, and tilts in so its muzzle points just right of and below the crosshair (hipYaw / hipPitch in js/vmanim.js,
blended out while aiming), which keeps the middle of the screen clear. The right hand is on the grip, the supporting hand under the handguard;
while aiming the supporting forearm tips down out of the sight picture.
Aiming down sights (right mouse) is camera-driven, like a real raised rifle: the weapon is drawn by its own camera, and sliding it from
the low hip position to the middle of the screen over a still world looked like a gun floating across the picture. Now the view turns
toward the sight (the world pans, the gun stays where it is relative to it) and the gun only makes the small remaining adjustment, so
the front sight ends exactly on the crosshair. How it works (js/vmanim.js, js/modes.js):
SPEC (post = front sight, else sight = rear sight), an eye relief er / erCentre (how far the rear sight is from your eye: the aimed rifle sits close, 0.22 m with centred sights / 0.30 m with the gun held to the side; handgun 0.19 / 0.23 m), a sight
yaw adsYaw, a share camShare (how much of the aiming movement the camera does, default 0.6) and, in js/weapondata.js, its own
adsFovMul (ADS zoom), adsIn / adsOut (blend times). Nothing is hard-coded per weapon: adsCamera(spec) measures the angle between
the anchor in the hip pose and the middle of the screen, turns the camera by camShare of it, and defines the gun's pose in the
turned view so that at full ADS the anchor is on the crosshair for any share (rifle: the view turns 7.2° / 2.3°, the gun itself moves 11 cm
instead of sliding 19 cm; handgun 5.2° / 2.9°). It is computed once per weapon and setting, then blended with the eased ADS progress.adsFovMul in js/weapondata.js;
ADS zoom scales it, and look speed is scaled with the zoom so aiming stays 1:1).sight: 'iron' weapons only).F quick melee, G quick grenade)The numbers follow Roblox RIVALS (community wiki / weapon database, checked October 2026; RIVALS is patched often, so
treat them as "as of then"). Everything is in one table, js/weapondata.js, shared with the server.
| Slot | Weapon | Role | Damage | Rate | Ammo |
|---|---|---|---|---|---|
| 1 | Assault Rifle (SCAR-style model) | full-auto hitscan | 12 up close, falling to 3 at range (headshot 15 → 4) | 0.10 s between shots | 20 in the magazine / 100 spare, reload 1.56 s |
| 2 | Handgun | semi-auto hitscan | 12 → 3 (headshot 15 → 4) | 0.13 s between shots | 13 / 91 spare, reload 1.06 s |
| 3 | Fists | melee, 2.4 m reach. Unlocks the double jump | 30 | 0.35 s | ∞ |
| 4 | Grenade | hold to aim, release to throw, or let the circle reach you to boost | 75 blast | 30 s cooldown, 1.2 s fuse | 1 |
Notes on the conversion: RIVALS measures distance in studs; here one stud is 0.36 m (RIVALS' avatar is ~5.2 studs tall, this
one is 1.85 m), so the rifle's 50 → 200 stud falloff is 18 → 71 m and the grenade's 16 stud blast is ~6 m. Health is
still 150 (RIVALS uses 100: set RV.MAX_HP = 100 in js/weapondata.js for its exact time-to-kill). RIVALS' per-weapon
move-speed changes are not copied: weapons never slow your movement here, by design. Spare rounds are refilled every
round / respawn; Practice has unlimited ammunition. With no rounds left the HUD says NO AMMO.
A deathmatch respawn takes no damage for 2 seconds, or until you attack (fire, punch or throw), whichever comes first. A readout under the crosshair counts it
down; a protected opponent glows faintly teal; shots at them are absorbed (no hit marker, a short "Spawn protected" note).
Falling out of the map still kills. In Duel and Online 1v1 there is no invulnerability after "FIGHT": the frozen 3 s countdown is the
protection (an invulnerable opponent made shots "do nothing", which looked like a hit-registration bug; the server's SPAWN_PROTECT_S
is now 0 by default). The 2 s respawn protection stays in Deathmatch (RV.SPAWN_PROTECT_S in js/weapondata.js).
VM.tracerOffset), so it follows the barrel of whatever model is in hand. Right mouse aims down sights
(hold, or toggle in settings): the gun turns in so its front sight is on the crosshair (see First-person weapon position). Only the picture
changes: the shot leaves through the crosshair either way.Left click punches (alternating hands, a short cooldown, one hit per punch; the hitbox is a slightly enlarged body box,
blocked by walls). Each fist is one thick rectangular block: a black sleeve and the hand on its end. A punch draws the fist
back, snaps it straight ahead to the crosshair with a quarter turn of the wrist, holds a beat and brings it back, while the other fist stays up
as a guard. Only while the Fists are equipped: press Space once more in the air for a second jump that rises exactly as high as a
normal jump (1.6 m). It refreshes when you touch the ground, keeps your momentum and air strafing (the held direction nudges
your course), shows a small ring effect (the fists themselves do not move for it), and never replaces a bunny hop or a wall jump. Switching away
turns it off at once; switching back mid-air never grants a second one if you already used it.
Quick melee (F): instead of switching by hand, press F: the fists come out, throw one punch as soon as they are up, and the weapon you had comes back (about 0.65 s in all: the fists are up in 0.2 s, the punch is over 0.2 s later, then the weapon comes back; the
"previous weapon" slot is left as it was). The fists are in your hands for that whole time, so a jump pressed in that time frame is a double jump - jump, tap F, jump again. With the fists already
out, F is just one punch. Choosing a weapon yourself during it ends it. Quick grenade (G): an instant throw with no animation: nothing is taken out, the weapon in your hand does not change, and the grenade just leaves along your view (no pin, no holding, no aiming sphere); with no
grenade left it only tells you when the next is ready. Both work online (the server sees the fists in your input and decides the punch like any other; the instant throw is its own request, nadeq, that needs a grenade in stock and nothing else), and both keys can be
rebound in Settings → Keybinds (the old Previous weapon key moved to V).
All four weapons are held by the same pair of block fists: one thick rectangular block each, a black sleeve (most of it) and a
skin-coloured hand on its end (buildFist in js/viewmodel.js; on the guns and the grenade they are drawn at 70-80 % so they never cover the
sight picture). They follow every animation: idle, run, slide, jump,
landing dip, firing (at the hip the rifle is thrown back towards you and springs forward past where it was, mostly along the barrel, with a small up-and-down a quarter swing behind it (about 1 cm against 5 cm back and forth), so each shot sends the gun round a small oval, never pitching it up or rocking it, and a full-auto burst keeps it circling; while aiming down the sights it does not shake at all (only its charging handle slides back), an aimed gun stays dead still while you slide, wall-run or run straight, but it has its own small sway: strafing angles it the way you move (right: turned and rolled to the right, left: to the left, about 3 degrees at full speed), a jump lifts it (up to 1.6 cm) and the fall lets it sink, and a landing bounces it a little (a few swings of about 1 cm that die away within half a second); the handgun does not shake: only its slide (the top) cycles; this is only the picture, your aim never moves: still zero recoil for the shot), reloads (the supporting hand pulls the magazine), weapon
switching, the grenade pin pull and throw and punches (the double jump has no hand animation). The weapon is drawn in its own pass, so it
never clips into walls.
Everything is synthesised (no audio files) and passes through a compressor and a small generated-room reverb. The assault rifle has its own layered shot (crack, body, low punch, tail, bolt tick; slightly different every shot); the handgun, reloads, punches, hit / headshot / kill ticks, grenade pin, bounce, fuse ticks, explosion and boost, footsteps, double jump, landing and UI all have their own sounds.
| Action | Default |
|---|---|
| Move | W A S D (arrows work too) |
| Look | Mouse (raw input, frame-rate independent) |
| Jump / wall jump / double jump (fists) | Space (hold for auto bunny-hop) |
| Slide / crouch | Shift or C |
| Shoot / punch / hold-aim grenade | Left mouse |
| Aim down sights (with the grenade out: also holds it) | Right mouse |
| Cancel a held grenade | R |
| Reload | R |
| Weapons | 1–4, mouse wheel, V previous weapon |
| Quick melee | F: the fists come out, one punch, your weapon comes back. A jump pressed while the fists are out is a double jump |
| Quick grenade | G: an instant throw with no animation (the weapon in hand does not change); with none left it says when the next is ready |
| Quick restart | T |
| Back to checkpoint / spawn | Q (time trial, practice) |
| Practice: set spawn | E |
| Scoreboard | Tab (hold) |
| Pause / release mouse | Esc |
play in
js/maps.js, enforced by World.move for the browser, the bots and the server alike): nothing can leave it, however fast it is launched, so a
grenade boost or a bunny hop over a wall top can never put you outside the map and into the void. The Arena also draws it: a faint
force field (barrier) stands above the wall tops up to the ceiling. Collision never throws a body to the far end of a solid any more (that
was the "I glitch and teleport out" bug: a body brushing the side of a catwalk or a wall while falling was lifted onto its top or pushed 20 m along
it; World._unstick now finds the nearest way out). (server/test/map.test.js fuzzes every map with extreme launches.)Two interface styles, switchable in Settings -> Interface -> Interface style (instant, remembered, never touches a match):
css/rivals-ui.css): chunky rounded panels with thick dark outlines and a hard button shadow, heavy rounded
lettering (no italics), a graphite base with orange as the primary colour (the colours of the assault rifle) and cyan for
information and for you. Its layout is its own: in the lobby a dock of five big buttons along the bottom (PLAY in the middle, keys
1-5), your name and the server status top-left, Records / How to play top-right and the details of the focused spot (the game modes, the weapons...) in a sheet that rises from the dock, centred above it; in a match the score card is top-left (timer, you, opponent, round), one long health bar (number on its left, colour from green through yellow and orange to red as health drops) sits bottom-left, the four weapon slots bottom-centre, the big white ammo count with the weapon name is bottom-right and the kill feed
top-right.css/theme-arcade.css): the earlier deep-navy, angled (chamfered) look with a cyan accent and italic numerals.
Both styles use the same components and ids; the RIVALS one is an <html data-ui="rivals"> layer on top. (The earlier Tactical theme and
its switch were removed at your request; its stylesheet is kept in _archive/ if you ever want it back.)The game opens in a small blocky plaza (js/lobby.js, modelled in Blender as js/mesh-lobby.js) drawn by the game's own
renderer. It has a PLAY gate in the middle (an animated portal on a stepped dais) with a MATCHMAKING board beside it, your
PLAYER podium (your avatar, the game's own blocky character, holding your start weapon, with your name over it), the LOADOUT
rack (the four weapons, the same Blender models as in first person, slowly turning), the PRACTICE range (lane, booth and
targets) and the SETTINGS control panel. Click a zone in the plaza or a button in the dock (RIVALS style: along the bottom; Arcade style: the side panel on the left; keys 1-5); the camera glides
to it (fixed cinematic stops with a little sway and mouse parallax: it can never clip a wall) and the panel shows what
the zone really does. The portal is a animated vortex (a noise-bent spiral with a hot core and drifting stars) that turns
violet/cyan when idle, orange while searching and green when a match is found:
| Zone | Real actions |
|---|---|
| Play | Find online match (the Online 1v1 queue), Duel / Deathmatch / Time Trial against bots (the normal map picker), a live server status line |
| Loadout | click a weapon on the rack (or its button): sets the weapon you spawn holding; Full weapon stats opens the Loadout screen |
| Practice | Enter the practice range (the Yard, free play), or pick another map |
| Settings | Mouse & aiming, Keybinds, Graphics (Potato mode), Network, and the 3D lobby switch |
| You | change name, your online record, Records |
Server online · 42 ms · 3 online from a live probe of the game server,
Server not reachable (offline modes still work) if it is down, or No game server configured (offline modes only) in dev mode.
Nothing in the lobby is a decoration: every button calls the same handler the classic menu used.?classic in the address.HUD: small exact crosshair; one top-centre bar you score | timer | opponent score; kill feed top-right; health
bottom-left; the four-slot hotbar bottom-centre (the selected slot lifts; grenade cooldown and double-jump pips on their
slots); weapon, ammunition and grenade count bottom-right; hit markers with an expanding ring, an elimination strip, damage
arcs and a quiet death screen. The HUD scales with the window (--hs) from 1024 x 600 to 4K; animations only touch
transform / opacity, and prefers-reduced-motion is respected.
Classic main menu - two switchable layouts (used when the 3D lobby is off; a slow 3D arena drifts behind the menu in both):
| B - Cinematic | D - Arcade Lobby | |
|---|---|---|
| Idea | A launch screen: attention goes to one large Play button | A multiplayer lobby: everything one click away |
| Play | Large Online 1v1 hero in the middle of the screen with the other four modes in a row under it | Online 1v1 hero and a 2 x 2 mode grid in a panel on the right |
| Navigation | Slim bar (Loadout / Settings / Records / How to play) between the header and the hero | Vertical button column down the left |
| Loadout | Reachable from the bar (the weapon list is left out to keep the screen clean) | Your four weapons as a strip bottom-left, under the Loadout button |
| Mood | Centred, symmetrical, lots of space | Side-weighted, compact, energetic |
How to switch (instant, no reload, never touches a match, the connection or your progress):
Menu layout [ B - Cinematic | D - Arcade Lobby ]; the current one is highlighted.The choice is saved in your browser (localStorage, key rivel.menuLayout) and applied before the first paint next time.
Implementation: <html data-menu="b|d">, set by js/menulayout.js; both layouts are CSS grids over the same buttons
(css/theme-arcade.css, section MAIN MENU LAYOUTS), so there is exactly one set of handlers. The 3D backdrop is
js/menubg.js: it reuses the game's renderer and the Arena map, renders at a capped 30 fps (20 on the Low preset),
and stops completely on every other screen, in a hidden tab and during a match. Below 901 px width both layouts fall
back to one stacked column.
Menus: main menu, map select, Loadout, pause (Resume / Quit full width, the rest in two columns), settings (tabs in
a side column), records, how to play, queue, username, match-end banner and results screens. Keyboard: arrow keys / Tab
move, Enter selects, Esc goes back.
js/post.js: HDR light, screen-space ambient occlusion, bloom glow, soft highlight roll-off, colour grade, sharpening, vignette and film
grain). It needs a good GPU and WebGL2 with float render targets (a device without them quietly draws High); the auto-quality governor switches
the effects off first when the frame rate drops. Smooth edges: hardware multisampling from Medium up (applies when the page loads), a cheap
FXAA filter on Low.index.html page + all menus / HUD markup (applies the saved main-menu layout in <head> before first paint)
css/style.css shared layout + components, driven by CSS variables (the Arcade palette)
css/theme-arcade.css the Arcade look: shapes, HUD, menu layouts B and D
vendor/three.min.js Three.js r159 (classic-script build, MIT licence in THREE-LICENSE.txt)
js/config.js backend address (SERVER_URL etc.): the only thing to edit before publishing
js/util.js helpers, storage
js/weapondata.js weapon table + damage rules (shared with the server)
js/settings.js settings, quality presets, key binding defaults
js/menulayout.js main-menu layout B / D: switch, persistence
js/menubg.js the slow 3D arena behind the main menu
js/stats.js records / personal bests
js/maps.js map definitions
js/mapdata-<id>.js collision + spawns of a map made in Blender (bridge, crossroads, docks, station, backrooms, darkarena), loaded by browser AND server
js/mesh-<id>.js the picture of a map / the lobby (testarena, lobby, bridge, ...), exported from Blender; maps' pictures load on demand
js/lobby.js the 3D lobby: scene, camera stops, avatar, rack, matchmaking board, zone cards, real server status
css/lobby.css the lobby's interface layer
js/models-weapons.js the weapons' first-person models (rifle, handgun, grenade; the hands are built in viewmodel.js), exported from Blender by tools/blender/export_models.py
tools/blender/ the Blender scripts that build and export models, maps and the lobby (run inside Blender); the .blend is _models/dashduel-models.blend
_models/exports/ glTF (.glb) copies of the lobby, maps and the shared kit
js/collision.js collision world (AABB + ramp wedges, spatial grid, raycasts)
js/grensim.js grenade physics + blast damage (shared with the server)
js/player.js movement physics (fixed 120 Hz step) incl. the fists-only double jump (also run by the server)
js/netproto.js the wire format both sides must agree on (input runs, flags)
js/input.js keyboard / mouse / wheel / pointer lock / rebinding / focus safety
js/audio.js WebAudio synth (compressor + reverb, no audio files)
js/vmanim.js first-person weapon + hands animation maths (pure, no THREE)
js/viewmodel.js weapon models, blocky hands, muzzle flash, shell casings
js/fx.js impact particles, decals, explosion / punch / double-jump effects
js/render.js Three.js scene: sky pass, merged map geometry, props, characters
js/icons.js inline SVG interface icons
js/hud.js in-game HUD
js/weapons.js the four weapons + loadout, headshots
js/grenades.js thrown grenades: aiming preview, flight, blast, boost
js/bots.js navigation graph + AI opponents
js/modes.js session loop, Duel / Deathmatch / Time Trial / Practice
js/netclient.js the one persistent WebSocket to the game server (handshake, heartbeat, latency, test)
js/online.js Online 1v1 client: input stream, prediction + reconciliation, interpolation, reconnect, rematch
js/ui.js menus, loadout, settings, records, keyboard navigation
js/main.js boot, main loop, frame-time governor
server/ the authoritative game server (Node.js, no dependencies) + 90 tests + .env.example: see server/README.md
docs/ONLINE_BACKEND.md backend choice, architecture, anti-cheat rules, configuration, deployment, limitations
Dockerfile container image for the server (Fly.io etc.)
_archive/ retired code (the old peer-to-peer transport, the Tactical theme and its switcher, the four retired maps)
cd server && npm test # 150 server tests (serial, ~6 min): movement prediction == simulation, combat, anti-cheat rules,
# sessions / reconnect / rematch, information protection, lobby, WebSocket layer, load,
# free-for-all rooms, the map vote, speed exploits, and a server that survives its own bugs
The browser side was verified with headless Chrome against the real server: two real browsers queueing, playing a full
5-round match (countdown lock, prediction with zero corrections, hidden opponent, server-confirmed hits, a dropped and
resumed connection, rematch, forfeit), both main menu layouts at many window sizes, switching layouts in the middle of a
match, and the connection-failure screens. Pointer lock and fullscreen need a real window, so check those by hand:
start a match, press Esc (mouse released), Resume, then pause menu → Fullscreen.
Licence: game code is yours to use; Three.js is MIT (see vendor/THREE-LICENSE.txt).
| 7 | Dark Arena | compact (27 x 39 m, 9 m walls) | a dark walled arena under a night sky, imported from the supplied Roblox model (_models/dark-arena/Dark arena.obj, scale 0.3 m per stud): slate-blue floor and cracked walls with the brown showing through, grey cover - a table on four legs in the middle, four turned slabs, two windowed arches, low blocks on the sides - and a tall wall in front of each end. Six spawns sit right behind that wall at each end, so the two players always start on opposite ends, hidden from the middle; a steep ramp in one corner of each end leads to a 4.5 m perch. Light strips along the wall tops, a blue pad and a red pad behind the end walls |
| - | The Yard | huge | practice field with a station for every mechanic (Practice mode) |
| - | Gauntlet | course | one-way time-trial course (Time Trial mode) |
js/online.jsserver/test/vote.test.jsConnected · <ms> · <n> online; Settings → Network shows the server
in use and has a Test connection button. You cannot pause an online match (the menu opens over the live match);
Leave match forfeits. If the opponent leaves they forfeit. If your connection drops the game reconnects by
itself (15 s grace; the opponent is told) and resumes the same round. When a match ends both players can press
Rematch.server/test/speed.test.js checks slide-jump chains, strafing, blasts at your feet and behind you, and that blasts do not stack).