
What it is, and how to run it
CORO SOLTO: Treta Suprema (formerly CS BRASIL) is a browser FPS written in vanilla JavaScript on top of Three.js r160, in the style of Counter-Strike 1.6: rounds, bots, AWP, Tab scoreboard, voice radio. It runs from a link, with nothing to install.
The numbers below are not hand-written: they are regenerated by
node tools/gen-docs.mjs from the code, and npm run docs:check (inside
check:fast) fails the gate when any of them diverges from the tree. Before that,
this page was aging at the very first commit — see
what is generated, and what is not.
| What | How much | Where to check |
|---|---|---|
| Game code | 32,001 lines in 44 files | git ls-files public/js/*.js | xargs wc -l |
game.js | 6,910 lines | wc -l public/js/game.js |
main.js | 2,698 lines | wc -l public/js/main.js |
| Weapons with GLB | 26 | git ls-files 'public/models/weapons/*.glb' | wc -l |
| Character GLBs | 45 | git ls-files 'public/models/characters/*.glb' | wc -l |
| Props in GLB | 108 | git ls-files 'public/models/props/*.glb' | wc -l |
| Versioned animation clips | 573 | git ls-files public/models/anims | wc -l |
| Playable characters | 44, in 5 factions | CHARACTERS array in characters.js |
| Maps in the registry | 12 | MAPS object in maps.js |
| Visual harnesses in HTML | 15 | git ls-files 'public/*.html' | wc -l |
| Harness scripts | 201 | git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' | wc -l |
| Pipeline scripts | 55 | git ls-files 'tools/*.mjs' | wc -l |
| Written entry tasks | 26 | git ls-files 'docs/issues/[0-9]*.md' | wc -l |
| Version | 2.0.0-alpha.184 | public/js/version.js and package.json (match) |
Block generated by
node tools/gen-docs.mjs. Source:the command in the right column of each row
And the match rules that move around the most, all read from the constants in
public/js/game.js:
| Rule | Value | Constant |
|---|---|---|
| Factions · characters | 5 · 44 (B 9 · C 9 · E 8 · F 9 · U 9) | CHARACTERS |
| Maps in the menu | 12 - 2 open in rounds, 10 in capture | MAPS / ctfMode |
| Respawn | 2.2 s | RESPAWN_DELAY |
| Round | 99 s, 3 wins | ROUND_TIME / ROUNDS_TO_WIN |
| Capture | target = all flags on the map, 2 rounds (480 s safety net) | capsToWin = ctfPts.length / CTF_ROUNDS_TO_WIN |
| Health regeneration | OFF - ?regen=1 turns it back on | REGEN |
Ranking / /u/ pages | OFF - controlled by one flag | RANKING_ON in src/lib/site.ts |
Block generated by
node tools/gen-docs.mjs. Source:constantes de public/js/game.js · RANKING_ON de src/lib/site.ts
The menu accepts from 2×2 to 8×8 bots (the engine accepts 1 to 8 per side); the default is 4×4.
Health regeneration was turned off on 05/08 (REGEN = QS.get('regen') === '1'). It
existed, CoD-style — 6 s without taking damage and 22 HP/s — and the owner reported it as
a bug ("the 1st player's health goes back to 100, I don't know why") precisely because
it was invisible: no icon, no sound, no line in the settings. A rule the player does
not notice is indistinguishable from a defect. It remains fully intact behind ?regen=1,
with player↔bot symmetry. Whoever turns it back on must ship the feedback along with
it — and solve what it had been papering over: with no healing, medkit, or armor, every
life after first contact was already lost.
The ranking was turned off and replaced with anonymous telemetry. /ranking and /u/*
respond 200 with a notice + noindex (not 404 — the URLs are indexed and will come back),
and /api/leaderboard responds {disabled:true}.
How many invariants pass is not derivable from the code — it is the result of a run,
and it even depends on which inputs exist on the machine. That is why that scoreboard is
not repeated here: it lives in the header of
KNOWN-BUGS.md, pasted
from a real run, with the list of red ones, root cause, and arquivo:linha for each one.
That is the file maintained day by day.
For today's state, run — do not repeat a number from memory:
npm run eval:vm && node tools/eval/invariants.mjs --json # 10-12 min
Order matters: a viewmodel invariant measured with yesterday's JSON invents a red (see How to contribute).
Run it in 3 commands
git clone https://github.com/rubenmarcus/csbrasil.git && cd csbrasil
npm install
npm run dev # opens http://localhost:4321 — this page IS the game
The audio pack (npm run fetch-audio) is optional: without it the game uses
synthesized sounds. The public/audio/ folder is not versioned.
Linux, WebGL, and compatibility mode
The game tries WebGL2 and WebGL1, starting with the browser default and reducing antialiasing, GPU preference, and stencil before giving up. WebGL1, llvmpipe/SwiftShader, or another degraded tier automatically uses low quality for that session: DPR 0.75, no bloom or shadows, and static portraits in character selection.
Use ?safe=1 to prioritize WebGL1 and the lowest-cost path. If it still cannot start,
inspect chrome://gpu or the Graphics section in about:support, enable hardware
acceleration, and update Mesa/the graphics driver through your distribution. A web page
cannot force a driver after the browser refuses to create even a WebGL1 context.
Alternative without Astro (zero build dependency)
The evaluation harness ships a 24-line static server that serves public/ and
maps / to the source of the game page:
node tools/eval/serve.mjs 8123 # http://localhost:8123
It exists exactly because src/pages/index.astro is pure HTML — you can serve the
raw file without going through Astro (tools/eval/serve.mjs:15).
The gotcha that costs everyone their first hour
There is no public/index.html. Serving the public/ folder statically gives you a
directory index with eval.html, mapview.html and company — none of them is the game.
The game's HTML is src/pages/index.astro, served at the root route by Astro. There
is no /game route.
The independent confirmation is in the harness itself: tools/eval/serve.mjs:15 needs a
special case if (p === '/') that reads src/pages/index.astro from disk, precisely
because there is no index.html in public/ to serve.
Until 04/08/2026 it existed because the root README.md told you to run
cd public && python3 -m http.server and spoke of a "game at /game/". Both lines
were fixed — today's README says the right thing. What remains is the fact itself, which
is still the first stumbling block for anyone arriving.
The real repository structure
Two code zones and a third zone that is the reason this doc exists (the harness):
No counts here: the tree says what each thing is, and the numbers live in the
generated table up top. Mixing the two is how the hand-written ARCH.md was born wrong.
public/ THE GAME — vanilla ES modules, ZERO build
js/
game.js the Game class (loop, bots, shooting, HUD) — the largest file in the repo
main.js menu, DOM wiring, persistence
vmattach.js springs.js weapons.js fparms.js handik.js viewmodel/weapons
maps.js the map REGISTRY (what is not here is not playable)
map_brasilia.js map_piscina.js map_havan.js
map_ferrovelho.js map_quebrada.js the registered maps
map_piscinao_ramos.js "Piscinão" — exists on disk, OUTSIDE the registry
mapprops.js map_decals.js props and graffiti
bloom.js textures.js vao.js stylize.js gpuparticles.js graphics/FX
characters.js glbchars.js characters
audio.js version.js site-bg.js
models/ weapons, characters, props and animation clips in GLB
vendor/ vendored Three.js (no CDN, no npm at runtime)
style.css the entire HUD
*.html visual harnesses (eval, mapview, weapontest, vm-inspect…)
src/ THE SITE (Astro + Vercel adapter)
pages/index.astro ⚠ THIS IS THE GAME (HTML + import map + HUD)
pages/sobre.astro landing/FAQ with JSON-LD
pages/personagens.astro como-jogar.astro ranking.astro mapa.astro
pages/u/[...path].astro public profile
pages/api/*.ts SSR: leaderboard, submit-match, register, badge, avatar
layouts/Layout.astro the site shell (not the game's)
lib/ supabase, svg, geo, fmt
tools/
eval/ THE HARNESS — rulers, gate and probes. See "Quality gates"
invariants.mjs the gate
ref-measure.py measures the reference frames (the house doctrine)
harness.mjs boots the real Game in node with stubbed DOM
ARCH.md BAR.md conflict map (generated) and the visual ruler
gen-arch.mjs generates and VALIDATES ARCH.md
gen-docs.mjs generates and VALIDATES the numeric blocks of this documentation
gen-asset.mjs generates a 3D prop from text (Tripo/Meshy)
gen-image.mjs generates 2D art from text (OpenRouter)
(database: schema/migrations are PRIVATE — outside the repo)
.github/workflows/ci.yml the gate running in CI
The maps registered today, and which mode each one opens in:
| Id | Menu name | Opens in | File in public/js/ | Lines |
|---|---|---|---|---|
praca_poderes | Praça dos Três Poderes | rounds | map_brasilia.js | 1,830 |
piscina_treta | Piscina da Treta | rounds | map_piscina.js | 810 |
loja_h | Loja H (Estacionamento) | capture | map_havan.js | 1,964 |
ferro_velho | Ferro Velho do Zé | capture | map_ferrovelho.js | 1,888 |
quebrada | Quebrada (Rua do Baile) | capture | map_quebrada.js | 1,599 |
posto_treta | Posto da Treta | capture | map_posto.js | 489 |
upa_24h | UPA 24h da Treta | capture | map_upa.js | 288 |
obras_prefeitura | Obras da Prefeitura | capture | map_obras.js | 240 |
atacadao_treta | Atacadão da Treta | capture | map_atacadao.js | 255 |
parque_treta | Parque da Treta | capture | map_parque.js | 402 |
velho_oeste | Velho Oeste da Treta | capture | map_velho_oeste.js | 433 |
penitenciaria | Penitenciária da Treta | capture | map_penitenciaria.js | 247 |
12 registered maps - 2 open in rounds and 10 in capture. ctfMode sets the initial mode; it does not lock it. There are 14 map_*.js files on disk, so a file alone does not make a map playable.
Block generated by
node tools/gen-docs.mjs. Source:objeto MAPS de public/js/maps.js
The two zones
In one line each: public/ is the game (vanilla, ES modules, no framework and no
bundler) and src/ is the site (Astro with SSR, where frameworks are welcome). What
each boundary rule pays for, and why it is hard, is in
Stack and tooling — a
single page, so that there are no two versions of the same boundary.
What you need to know before editing is the consequence: the game is loaded by the
Astro page through an import map versioned by content hash (src/pages/index.astro).
scripts/module-cache.mjs hashes the published modules under public/js/, and the import map
applies that revision to the entire graph. Do not bump it manually or include benches
removed by scripts/prune-dist.mjs. npm run eval:shaderbudget (SB7) checks both properties.
Commands you will use
npm run dev # site + game (Astro, :4321) — the / route IS the game
npm run build # dist/client + dist/server
npm run eval:vm # viewmodel framing — RUN BEFORE the invariants
npm run eval:invariants # the invariants — pure node, 10-12 min
npm run eval:bots # botsim 60 s per map, fixed seeds
npm run eval:mat # material/light/fog/texture on the maps
npm run docs # regenerates the numeric blocks of this documentation
node tools/eval/serve.mjs 8123 # static server without Astro
And the two gates, with the exact list of what each one runs — straight from package.json:
npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval:pickuparma eval:parquewheel eval:redesign eval:matchoptions eval:charvoice eval:screenquery docs:check arch:check audio:check feet:check eval:vmlabhud eval:ctfhud eval:pause eval:ctfround eval:ctfwin eval:spawn eval:regen eval:pegada eval:dmgdir eval:ctflabels anims:check anims:merge:check walls:check media:check menuwalls:check travessao:check eval:medianet eval:posters eval:grafitelayout eval:simclock eval:backendhints changelog:check eval:velhooeste eval:penitenciaria eval:mutcega eval:autofix eval:deploygate eval:portaointeiro eval:wfsecret eval:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam
package.json has 124 scripts; the reason behind each one lives in SCRIPTS.md.
Block generated by
node tools/gen-docs.mjs. Source:node -p "Object.keys(require('./package.json').scripts)"
npm run check is the same set the CI runs in .github/workflows/ci.yml.
check:fast in the loop, check before the PRcheck takes 10-12 min because it boots the game five times. check:fast covers the
rulers (quality gates) that were born from the most recent bugs (pause menu, capture
round, regeneration, animation manifest) and runs in about a minute.
Where to go now
The sidebar order is the reading order, and each page delivers one thing:
- Stack and tooling — what this is built with, with the declared version
of each piece. It is where the
public/×src/boundary is explained in full. - AI instrumentation — how the work gets done here. If you have never collaborated with agents in a repository, start with this one.
- The gate — what an invariant is, how to write one, the two house laws, and the mutation test of the ruler itself. It is the most useful page on the site.
- Architecture — how N agents edit the same file without
colliding, and the conflict table. Read it before touching
game.js. - How to contribute — what a PR needs to get in, and the
first-contribution tasks already written in
docs/issues/(with a ready-madeabrir-issues.sh— they have not been opened on GitHub yet). - License — the repo-root
LICENSEdeclares it (AGPL-3.0 today); the surfaces that repeat its name and must change together are listed inCONTRIBUTING.md. - Current state — live sources for production health, data coverage, and debt since the last pasted measurement.
Where the project is going is not in this documentation: it is
docs/ROADMAP.md, and the
executable plan is
plans/08.