Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A browser tank game with destructible cover, team battles and solo survival. The engine is Rust compiled to WebAssembly: the simulation runs on Rapier, a custom wgpu renderer draws with handwritten WGSL on WebGPU, and a TypeScript page shell handles menus, input and Howler audio. Multiplayer rooms run the same Rust simulation in a native Rust server.
Use Node.js 24 or newer, pnpm 11 or newer, and Rust through rustup. On macOS:README 전문 보기 ↗A browser tank game with destructible cover, team battles and solo survival. The engine is Rust compiled to WebAssembly: the simulation runs on Rapier, a custom renderer draws handwritten WGSL through WebGPU (wgpu), or WebGL2 (glow) where WebGPU is unavailable, and a TypeScript page shell handles menus, input and Howler audio. Multiplayer rooms run the same Rust simulation in a native Rust server.
Use Node.js 24 or newer, pnpm 11 or newer, and Rust through rustup. On macOS:
brew install node pnpm rustup # once per machine
rustup toolchain install # in the checkout: the toolchain rust-toolchain.toml pins
cargo install wasm-bindgen-cli --version 0.2.129 --locked
pnpm install # once per checkout or worktree, and after dependency changes
pnpm run wasm # build the engine; rerun after Rust or WGSL edits
pnpm run dev
rust-toolchain.toml pins the Rust release, rustfmt, clippy and the
wasm32-unknown-unknown and x86_64-unknown-linux-musl targets. The wasm-bindgen CLI must match the wasm-bindgen crate pin
(0.2.129). Homebrew's rust formula provides a cargo that ignores
rust-toolchain.toml, so rustup's must come first on PATH:
export PATH="/opt/homebrew/opt/rustup/bin:$HOME/.cargo/bin:$PATH"
On other systems, npm install -g pnpm also works. package.json pins the exact pnpm version, which pnpm fetches for itself, and npm refuses to run in this repository.
Optionally, cache compiler outputs across checkouts with Kache,
which pays off when every change gets its own worktree. Set it as Cargo's wrapper in your
user-level ~/.cargo/config.toml (not the repository's, so CI stays plain):
cargo install kache --locked
printf '[build]\nrustc-wrapper = "%s"\n' "$HOME/.cargo/bin/kache" >> ~/.cargo/config.toml
A new worktree then restores whatever another checkout already compiled instead of
compiling it again, at the cost of about a second per rebuild within one tree.
Bypass it for one command with KACHE_DISABLED=1; kache explain says why a crate
missed the cache.
Open the URL Vite prints, normally http://127.0.0.1:5173/sloppy-tanks/. Vite
serves the release Wasm from src/generated/engine/ (and the WebGL2 build from
src/generated/engine-webgl/); it does not compile Rust,
so run pnpm run wasm again after changing a crate. That rebuilds the WebGPU engine
only (the WebGL2 one too when it is missing or the engine's API changed); run
pnpm run wasm:webgl before trying ?webgl after an edit.
Rendering uses WebGPU where the browser offers it (HTTPS or localhost, and a supporting browser and GPU). Elsewhere the page loads a separate WebGL2 build of the engine instead; WebGPU browsers never download it. Add ?webgl to the URL to try the WebGL2 build on any browser. Stats for nerds (on a page opened with ?debug) shows the graphics API in use and rendering diagnostics.
To try a build on a phone or tablet, pnpm run tunnel builds the game and prints a temporary https://….trycloudflare.com/sloppy-tanks/ link (needs brew install cloudflared; see docs/cloudflare-tunnel.md). Anyone with the link can open it while the command runs; restart it after changing the source.
The game also comes as a desktop app for Apple silicon Macs and Windows: download it from the latest release. The same app builds for iPad (see iPad).
Choose Skipper, Bruiser or Big Rig, then select a map and difficulty:
Your tank, single-player mode, map and difficulty are remembered in this browser. The overhead/first-person view and overhead zoom are remembered too, and shared between single player and multiplayer. Playing an extra level or an online team battle keeps your preferred single-player mode for the next standard map.
Map links accept ?map=village, ?map=harbor or ?map=quarry.
Opening the game with ?debug adds two stress levels, marked EXTRA, to
the map dropdowns: Stress Grid (30 tanks among 75 destructibles) and
Scrap Yard (30 tanks in a compact yard whose cover rebuilds). In single player
they are an endless 15 v 15 team battle with your chosen tank; online they follow
the room's rules. Players are nearly invulnerable on both, and pickups are ten
times stronger. Rooms on these levels appear in the room list only on such a page;
their room links work for anyone. ?debug&map=superstress links straight to
one.
| Control | Action |
|---|---|
| WASD / arrows | Steer toward a screen direction; opposite input reverses |
| Mouse | Aim the turret |
| Hold left click | Fire |
| Right click | Drop a mine |
| 1–5 / ammo buttons | Select Standard, Spread, Rocket, Ricochet or Piercing |
| Q / E / scroll | Cycle stocked ammunition |
| Shift + scroll | Zoom |
| V / ◎ button | Toggle the first-person view from the turret |
| Escape / Pause | Pause |
| ⚙ button | Settings: touch controls, sound, graphics, battle speeds |
Settings' Graphics: Auto / High / Medium / Low is saved like the other choices. Auto keeps the frame rate smooth: desktops start with the full picture, while tablets and phones start on Medium at one rendered pixel per CSS pixel without multisampling. As the frames allow, Auto lowers or raises the 3D scene's resolution in small steps and the quality tier with it. High is the full picture; Medium drops ambient occlusion and fine ground and cloud detail; Low also drops bloom and halves the shadow map. A fixed tier draws at full resolution and never adapts, and High also keeps multisampling on a tablet after a reload. Stats for nerds (?debug, then N) shows the tier, the render scale and the GPU's frame time.
In first person the camera sits on your turret: the mouse turns it (click the arena to capture the pointer; Esc frees the cursor and a second Esc pauses), WASD steers relative to where you look, the compass above the ammo strip shows which way the hull points, and pickups turn see-through. On touchscreens the drive stick's sideways push turns the view (forward and back drive toward it), and dragging the finger that fires fine-tunes the aim. While destroyed, you watch from above and the pointer stays captured for the respawn; press Esc to pick another tank.
Losing focus clears held input, while a hidden page pauses the round. Standard ammunition is unlimited; collect crates for special ammunition. Power-ups provide rapid fire, speed, shields, repairs and automatic laser defense. Enemy hull damage and kills earn Veteran, Elite and Heroic ranks; death resets rank progress. Timber and cargo break apart, drums explode, and destroyed cover opens routes. Allied tanks block shells without losing hull health or shields; rockets detonate on contact, with their existing self-damage rule. Bots hold fire when an ally blocks a firing lane, including spread pellets.
On iPads, car screens and other touchscreens, touch controls appear automatically: the same scheme as the phone edition, with the full game. Drag the stick in the bottom-left corner to drive (shorter drags move slower), and touch the arena to aim and fire: the turret turns toward your finger and fires until it lifts, and the finger can stay down and slide. Tap ✹ in the bottom-right corner to drop one mine, tap an ammo slot in the weapon strip between them to select it, and use − / + to zoom. The mine button shows its cooldown. Open Settings with the gear in the top right corner to choose Touch controls: Auto / On / Off; the preference is saved. Landscape gives the clearest view, and portrait is supported. The joystick UI and its styles load only when touch controls are enabled; desktop Auto and Off skip their downloads and hidden UI updates.
Phones (a touchscreen whose shorter side is under 600 px, or any device with ?phone) get a limited edition: Battle Setup offers only the tank, the map, the two tabs and the battle format (Team Battle or Solo Assault), single-player bots are always Easy, and the arena shows nothing but the drive stick, a small scoreboard, − / + zoom in the top-left corner, the first-person ◎ and pause buttons in the top-right, and the mid-screen notices. Touch the arena to aim and fire: the turret turns toward your finger and fires until it lifts, and the finger can stay down and slide. In first person (◎, remembered on the phone), the drive stick turns instead of strafing: push it sideways to turn the view (the tank follows when driving) and forward or back to drive; touching the arena fires, and dragging that finger fine-tunes the aim. The gun sight and hull compass show. Phones start with a farther camera until you zoom (the zoom is remembered), draw no overhead aiming reticle, and neither zoom the page nor show the long-press magnifier. Multiplayer on a phone is one button with no room list: it joins the busiest open room (one whose battle is under way first), showing that room's map, or creates a room on the chosen map with bots filling the teams; a room link joins that room. In a room the menu keeps only the room code, what is happening, the results and the actions: room rules, team and tank changes need a larger screen. There is no mine button, ammo strip or Settings on a phone.
Team-only HUNTER Humvees make hit-and-run TOW attacks. They prefer isolated targets, plan an escape before firing, and withdraw behind cover (or open distance when no cover is available). After reloading and a short pause, they approach from a different position. They remain lightly armored and do not escort the player. They stop for 0.9 seconds to aim and stay exposed for 0.65 seconds after launch; a TOW deals 75 base damage. Most Humvee kills erupt in a fireball and tumble as a whole vehicle; roughly one in five instead leaves a quietly smoking wreck with a small hop.
Choose END BATTLE from the pause menu to finish early and see your current stats without declaring a winner. The game-over screen is a battle report: eliminations, busiest rolling minute, longest life, best killing spree, damage dealt, highest rank, average kills per minute, direct projectile hit rate, five-second multikills, low-hull kills, revenge, previous-life ordnance kills, mine kills, demolition, pickups, hull damage taken, and shield damage absorbed. Earned callouts celebrate feats such as ONE-TANK ARMY and DEAD BUT DANGEROUS. Survival time excludes pauses; direct hit rate excludes splash-only hits and counts spread pellets individually. Personal bests are saved in this browser separately for each mode, map, and difficulty. PLAY AGAIN immediately starts another round with your current settings; BATTLE SETUP returns to the menu.
pnpm run check # the CI gate: formatting, Wasm and Vite builds, lint, types, clippy, server, all tests
pnpm run wasm # release WebGPU engine and its glue into src/generated/engine/
pnpm run wasm:all # both engines, the WebGL2 one into src/generated/engine-webgl/
pnpm test # page shell, import boundary and traffic-bot tests (needs pnpm run server:build)
pnpm run test:rust # simulation, net, renderer and server tests (cargo test --workspace, without the desktop app)
pnpm run validate # ten seeded full matches and reset checks
pnpm run build # production output in dist/ (builds the Wasm first)
pnpm run lint:fix # safe ESLint fixes
pnpm run format # Prettier for source, tests, scripts, styles and docs; cargo fmt for Rust
pnpm run check:browser # browser checks against a running dev server (set SLOPPY_URL)
AGENTS.md is the development guide: code conventions and the simulation, determinism and rendering rules. scripts/README.md lists the browser checks and performance measurements. The engine was ported from the TypeScript game at 35afd91 (in Git history) and differs from it on purpose in Rapier 0.36's contact response, zlib-rs level 2 WebSocket compression, square cargo fittings on the three moored harbor ships and ownerless damage sent as owner: 0.
| Crate / directory | Target | Owns |
|---|---|---|
crates/core | native + Wasm | Simulation, rules, bots, maps (sim/), meshes and models, multiplayer protocol (net/) |
crates/render | Wasm | The WebGPU/WebGL2 renderer, WGSL shaders, presentation, effects and cameras |
crates/web | Wasm | The wasm-bindgen API: Game (single player) and NetGame (a room page) |
crates/server | native | The multiplayer server: HTTP, WebSocket rooms, limits, monitor and dashboard |
crates/desktop | macOS/Windows | The desktop app: a native window on wgpu (Metal, Direct3D 12), egui menus, HUD |
src/ | browser | The page shell: menus, HUD, input, touch controls, audio and the room page |
| Area | Starting points |
|---|---|
| Startup and game loop | src/main.ts, src/game.ts, crates/web/src/game.rs |
| Simulation and match lifecycle | crates/core/src/sim/simulation.rs, crates/core/src/sim/match_state.rs |
| Driving, weapons and damage | crates/core/src/sim/{tank_driving,weapons,projectiles,damage}.rs |
| Bots and navigation | crates/core/src/sim/{ai,bot_strategy,bot_movement,navigation}.rs |
| Maps and scenery | crates/core/src/sim/maps.rs, crates/core/src/models/scenery.rs, crates/render/src/presentation/{model_catalog,theme}.rs |
| Models and materials | crates/core/src/models/{tank_model,cover_model}.rs, crates/render/src/{model,material,shader}.rs |
| Balance and progression | crates/core/src/sim/{data,combat_rules,difficulty,veterancy}.rs |
| Multiplayer | crates/core/src/net/, crates/web/src/net_game.rs, src/net/client.ts, crates/server/ |
| Controls, UI and sound | src/game/controls.ts, src/game/ui.ts, src/game/audio.ts |
The development build exposes window.sloppy for diagnostics; ?tweak opens the development-only zoom panel. ?autoplay assigns bot controls to the player slot.
Multiplayer is available on the production site and the dev site, each with its own game server. Battle Setup's tank and map choices sit above its two tabs, Single player and Multiplayer, and stay put when you switch: the tank you pick is the one you drive online, and the map is the one a new room plays. The Multiplayer tab lists open rooms (it polls only while shown) below a New room row that holds the new room's match length and Humans only choice. Your saved name is prefilled; first-time players get a random bot name they can edit. Auto picks the team with fewer human seats (including reconnect reservations); choosing Blue or Red repaints the tank previews. The button where single player has GO! reads Create room for the new room, which starts playing immediately, or Join room once you choose an open room. Battle Setup stays up with the room's progress while the arena loads and draws its first frames, then the battle replaces it; a page that has already built a single-player arena reloads into the room behind the same setup. Friends can join later through the list or a link from Copy invite link in the in-game menu: a room link opens Battle Setup with that room selected. Reloading a room page, or a join that gives up, returns there too, with the reason shown in the room list; joining again keeps a seat that is still reserved. Listings show human player counts, map, bot mode, round time and score.
The online HUD shows power-up time remaining, highlights critically low hull, and explains who destroyed you and which weapon caused it during respawn.
Online the battle keeps playing behind the in-game menu and Settings: while either is open a bot drives your tank (in humans-only rooms it idles), and closing them takes it back. The in-game menu shows the room's rules as text. Between rounds it leads with the last round's result; everyone stays in the room, so Play again keeps the group and its teams together, while Battle Setup leaves the room (closing it if you were the last player). The host can unfold Change rules, and anyone can change team or tank for the next battle. If the connection drops, a dialog replaces the menu while the game reconnects on its own. If it gives up, the dialog says why (lost connection, room closed, seat expired, seat opened in another tab, or a game update) and offers what still works: try again, play here, reload, or return to Battle Setup.
New rooms default to Humans only (no bots). Empty seats stay empty, and paused or disconnected players remain idle and vulnerable. Uncheck it when creating, or pick a bot difficulty between rounds, to fill the six-versus-six teams with bots; bots also drive absent humans in that mode. Up to eight people can join, with six human seats per team. Reconnect within 30 seconds to reclaim the same seat.
The last explicit departure removes the room and frees its simulation. An accidental disconnect hides an empty room from the list while retaining the 30-second reconnect grace. The host can end a round or choose settings for the next round. Accounts and saved matches are not required; a server restart ends the current match.
Stats for nerds is available during multiplayer battles on a page opened with
?debug: click the bottom-right link or press N. Network rows show RTT, received update count/rate, update
age, server tick and input sequence sent/acknowledged. A received update is one
full-state message or snapshot batch; an input acknowledgement confirms the
server processed an input sequence. Feel rows show how far behind your own tank
other tanks are drawn (view delay), how long a click takes to show its muzzle
flash, how many of the shots drawn at once the server fired too and how close
their shells were to the server's, how far the
server's impact was from a tank you hit as your screen drew it (the server judges
your shells where you saw the targets, up to 200 ms back), how far the shells that hit
you were drawn from your tank just before they struck (enemy shells close in on your
tank's present), and your tank's prediction corrections by cause. Render includes GPU geometries and textures.
Single-player downloads no multiplayer page code and opens no game-server connection. Multiplayer loads its client and UI only on entry and does not run browser physics. The plan records remaining playtest gates and the server guide describes local development and deployment.
crates/desktop is the same game as a native app for macOS and Windows, no browser:
the Rust Game and NetGame the page drives, drawn with wgpu on Metal or Direct3D 12,
with menus and HUD in egui and the textures and sounds from
public/ built into the executable.
The window opens at 1280×800 points and has a minimum of 1100×720. Its menus and HUD
follow the web edition's desktop layout; fullscreen and Quit are native controls.
pnpm run desktop # the app for this machine
pnpm run desktop:mac # Sloppy Tanks.app for Apple silicon, macOS 11 or newer
pnpm run desktop:windows # sloppy-tanks.exe, on Windows with the MSVC build tools
Both land in the ignored artifacts/desktop/: the Mac app beside a zip of it and an
installer package (.pkg, which puts it in /Applications) for sharing, and the
Windows executable. Build through the script rather than cargo build: it stamps the
content version multiplayer joins send, which must match the servers' (the same one the
page carries), as well as the release and commit Battle Setup shows. It adds the Apple silicon
and Windows targets with rustup when they are missing and uses the desktop Cargo profile, which
optimizes everything for speed with fat LTO and one codegen unit. Pull-request artifacts use
ThinLTO and parallel code generation to compile faster; main's releases and local builds
keep the full release optimization. CI keeps separate caches for these compiler settings:
each PR's first app build seeds its ThinLTO cache, and later pushes reuse it. Release
builds use the fat-LTO cache from main. cargo run -p sloppy-desktop works for quick local
tries, but servers refuse its unstamped content version. The
Desktop app workflow builds both, and the iPad app for
the simulator, on pull requests that
touch the crates or assets and on main, and keeps them as run artifacts, along with Cargo's
compilation timing reports. macOS clippy and shell tests run alongside the app builds in
the desktop-check profile, without optimizing the GPU and window dependencies, while
retaining workspace line tables for panic backtraces. Linux CI leaves
the crate out (--exclude sloppy-desktop), since its window and audio crates need
system libraries there. Main's builds also replace the repository's
latest release, under file
names that stay the same, so these links always fetch the newest:
Mac installer,
Mac app (zip)
and Windows.
The release job publishes only a Mac app and package that carry a stapled notarization
ticket.
The same crate builds as an iPad app (iPadOS 16 or newer, landscape, full screen), with the web edition's tablet touch controls: the drive stick in the bottom-left corner, a finger on the arena to aim and fire, and the mine button with − / + zoom in the bottom-right corner. A hardware keyboard plays as on a Mac (arrows or WASD, Space, 1–5, Q/E, V). There is no Quit or fullscreen, and single player pauses when the app leaves the screen. It needs a Mac with Xcode:
pnpm run ipad # artifacts/ipad/: Sloppy Tanks.app and an .ipa for devices
pnpm run ipad -- --install # also install and open it on the connected iPad
pnpm run ipad:simulator -- --install # artifacts/ipad-simulator/, opened in an iPad simulator
A device build is signed with an Apple Development identity and the provisioning profile
Xcode keeps for the bundle identifier (the team's wildcard profile covers it), and
installs only on the devices that profile lists; SLOPPY_IOS_PROFILE and
SLOPPY_IOS_IDENTITY choose others, and SLOPPY_IOS_DEVICE the iPad to install on.
Simulator builds need no signing. Like the Mac app it carries the content version, so it
joins the same multiplayer servers.
Mac builds from main, and local ones on a Mac holding the Developer ID certificates and
a notarytool profile, are signed and notarized, so the .pkg and the zip open without
a warning. Pull request builds carry only an ad-hoc signature: the first time,
right-click the package or the app and choose Open, or, once macOS has refused it,
choose Open Anyway in System Settings → Privacy & Security. The Windows executable
is unsigned, so SmartScreen asks once: More info, then Run anyway. Start it with
--debug (for example "Sloppy Tanks.app/Contents/MacOS/sloppy-tanks" --debug) for
the extra levels a ?debug page offers. Settings and personal bests live in
~/Library/Application Support/Sloppy Tanks/ on macOS and %APPDATA%\Sloppy Tanks\ on
Windows.
Runtime textures, tank previews and sounds are checked in under public/. Development and production builds use these files directly. Regenerate them only when changing artwork or sound:
| Command | Output / requirements |
|---|---|
pnpm run generate:textures | Procedural pickup, house, barrel and armor artwork, followed by texture optimization; requires cwebp |
pnpm run optimize:textures | Optimized runtime textures from preserved sources; requires cwebp |
pnpm run generate:ammo | The four special-ammunition pictograms; requires cwebp |
pnpm run generate:pickup-atlas | The shared pickup atlas, painted procedurally by its generator script alone; also run by generate:textures and generate:ammo |
pnpm run generate:previews | Tank selection WebPs rendered from the actual models; requires Google Chrome |
pnpm run generate:audio | Thirteen MP3 effects; requires FFmpeg |
pnpm run generate:favicon | public/favicon.svg, drawn as isometric vector shapes |
pnpm run generate:app-icons | The Home Screen icons in public/icons/: the favicon's tank on navy; requires Google Chrome |
On macOS, install the offline encoders with brew install webp ffmpeg.
Source images and encoding guidance live in assets/texture-sources, including harbor, quarry and tree notes. The optimizer uses 768px grass and sandstone, 512px dirt and concrete, and 512px armor wear and conifer foliage. Source artwork is outside the deployed directory.
For conifer artwork, run node --import tsx scripts/generate-conifer-texture.ts followed by pnpm run optimize:textures. Other tree patterns use node --import tsx scripts/generate-tree-textures.ts. Rebuild the tree atlas after either with node --import tsx scripts/generate-tree-atlas.ts. The cottage and watchtower surfaces (notes) use node --import tsx scripts/generate-house-textures.ts, then pnpm run optimize:textures.
See tank references for model provenance and water texture notes for its source and license.
A GitHub Pages workflow publishes on pushes to main, after the quality gate passes: the
pull-request check workflow's parallel jobs, which together run everything in
pnpm run check and build the site.
It builds dist/ with DEPLOY_BASE=/ for https://sloppy-tanks.fridman.me/, the
repository's Pages custom domain (a DNS-only CNAME to vladf1.github.io in
fridman.me's Cloudflare zone). The old
address https://fridman.me/sloppy-tanks/ redirects there. Local pnpm run build and
the Vite dev server keep the default /sloppy-tanks/ base.
The workflow sets VITE_DIRECTORY_URL to the production server directory, so the page
holds no server address. The directory is a Cloudflare Worker (directory/, published with
pnpm run directory:deploy) that every game server pushes its rooms to; the page lists
every server's rooms from it and connects to the server that hosts the chosen room, or for
a new room the one with the fewest rooms. A machine pushes its rooms to the directory
under its environment (production or dev), which its startup script
(deploy/server/cloud-init/) writes as DIRECTORY_URL and SERVER_ENVIRONMENT; the build names the page's as
VITE_SERVER_ENVIRONMENT. The workflow does not deploy the server, but after the site deploys it
points the server's container image tag :production at the commit's image; the dev
site uses a separate server on its own machine (below). A
client only plays on a server built from the same shared sources: the crates/core
files and the crates they compile with, listed by node scripts/content-version.mjs,
and pnpm run server:check-if-redeployment-required says whether the live server
matches this checkout and warns when the live page cannot join it. Both report their
build at /health: release version (version), protocol, content version, commit,
local-change state and build time, and the server also its server build. The release
version is MAJOR.MINOR.PATCH.BUILD, such as 1.1.0.628: version in package.json,
bumped by hand, and the Pages workflow's run number, which GitHub increments on every
deploy from main (local and pull request builds have only the first three parts).
Battle Setup shows it with the commit. The page's /health is a static
health/index.html that GitHub Pages serves after a redirect to /health/
(https://sloppy-tanks.fridman.me/health). The server's release, commit and time are
those of the first main build that shipped its server build, so they predate the
page's when later commits left the server unchanged. After merging changes to any of them, run
pnpm run server:update (or let auto-update pull it); until then, players on the
production site are asked to reload and cannot join. Server-only changes
(crates/server, its dependencies and build settings) keep clients compatible but
still need that update to take effect; client-only files (crates/render,
crates/web, the page shell) never do. See crates/server/README.md for the
updater, auto-update and the SSH fallback.
DEPLOY_BASE controls both Vite asset URLs and the engine download.
The build separates the interactive menu from the engine and audio. Battle Setup appears immediately with a progress strip while the engine, textures and hidden arena prepare. Pressing GO early changes the button to WAIT and confirms that the round will start automatically; there is no need to keep clicking. The arena's shaders and first frame are prepared before combat starts. GO reuses the prepared arena when its choices still match, or prepares the newly selected map while keeping the menu visible. Independent image downloads, device setup and GPU pipeline compilation overlap where possible. The engine (Rust simulation and WebGPU renderer) is one WebAssembly file, emitted as a separate hashed asset and requested from the page's head so it downloads while the menu loads. A second file holds the WebGL2 build, which the head script requests instead only when the browser gives no WebGPU adapter. Hosts should serve it as application/wasm with gzip or Brotli compression.
pnpm run deploy:dev runs the normal checks, builds the current local checkout
(including uncommitted changes), and publishes it to the separate sloppy-tanks-dev
Cloudflare Worker. Run pnpm exec wrangler login first (or provide an API token that
can deploy Workers and their custom domains), and log Docker in to ghcr.io once with a
token that can write packages (crates/server/README.md shows how with gh). The
Worker's account, name and domain come from deploy/dev-site/wrangler.jsonc,
independent of the local Git branch. No Git commit or push is required.
pnpm run build:dev builds without publishing.
The dev build connects the Multiplayer tab to the dev directory, where the dev
multiplayer servers list themselves. They run on their own machines, each at its nip.io
name (wss://1-2-3-4.nip.io, dashboard at /dashboard). deploy:dev first pushes
the same checkout's server image as its :dev tag, which those servers check every 10
seconds and switch to by themselves, and waits for them, so client and server versions
match; production multiplayer is never affected. Unlike production, the
dev site keeps the development network tools: a room page takes ?latency=, ?jitter=
and ?stall= (milliseconds of simulated transport delay), &latencySlider shows sliders
that change them mid-battle, and ?dodge=0 / ?shooter=0 turn off the fairness options for
comparison.
The dev game is at https://sloppy-tanks-dev.fridman.me/ and the directory of
browser test pages is at https://sloppy-tanks-dev.fridman.me/test-pages.html.
The provider URL is https://sloppy-tanks-dev.vova145.workers.dev/. Its /health
reports the same build details as production's, including whether local changes were present.
The build goes to dist-dev/ with the / base; production builds and deployments
stay separate. The hostname is the Worker's custom domain: Wrangler keeps its DNS
record in fridman.me's Cloudflare zone, and all other records stay as they are.
The Worker serves dist-dev/ as static assets with the build's _headers (immutable
/assets/, noindex everywhere). Only the engine binaries run its script,
deploy/dev-site/worker.mjs. Cloudflare compresses static assets as it serves them,
which gives the 3.7 MB engine a later first byte and a larger file than Brotli at
quality 10. So deploy:dev first writes Brotli-10 and gzip copies beside each .wasm
(deploy/dev-site/precompress.mjs, a few seconds), and the script sends the one the
client accepts, marked no-transform. Range requests, and clients that accept neither, get the
original file.