From 2f0f7623969c4248d83de7307486d3219af9b9f4 Mon Sep 17 00:00:00 2001 From: Emerson Garrido Date: Fri, 21 Aug 2026 15:18:46 -0300 Subject: [PATCH 01/16] =?UTF-8?q?feat:=20fuma=C3=A7a=20do=20cano=20+=20tra?= =?UTF-8?q?nco=20e=20c=C3=A1psulas=20mais=20fortes=20no=20tiro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aproveita a ideia de um FPS de referência (fumaça, cápsulas voando, tranco por tiro) reusando a infra que já existe — sem lib nova, zero alocação por tiro: - fumaça do cano por tiro em _flash(): GPUParticles via _tintFx (compartilha uTime/uScale do puff; em quality:low degrada pro puff branco, sem draw call novo). Menor/mais perto na 1a pessoa pra não virar blob na lente (padrão R7.6). - tranco de tela mais perceptível: _shotRecoil sobe o teto/ganho de st.sh (0.013/0.16 -> 0.020/0.22) e _updatePlayer aplica um roll leve no eixo Z reusando o mesmo st.sh decadente (freq 61 vs 78 do pitch) => "tremida", não só punch vertical. Vale pra mouse e toque; respeita ?gunfeel=0 e ADS. - cápsulas mais visíveis: geo maior (0.016x0.05, 8 seg), brass com metalness 1.0 + emissive leve, arco um pouco maior e ttl 1.6s -> 2.2s. - _fxTune ganha campo smoke pra tunar/desligar a fumaça via dev tooling. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Emerson Garrido Agent: Claude Code --- public/js/game.js | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/public/js/game.js b/public/js/game.js index 14bd9770..528e0919 100644 --- a/public/js/game.js +++ b/public/js/game.js @@ -942,7 +942,7 @@ export class Game { this._vmFlashLight.position.set(0.1, -0.06, -0.75); // boca do cano em view space (pose GAUNTLET 2.0) this.vmScene.add(this._vmFlashLight); this._vmFlash = { t: 1, life: 0.045, peak: 1.6 }; - this._fxTune = { light: 1, flash: 1, spark: 1 }; // multiplicadores de FX (dev.html game-backed) + this._fxTune = { light: 1, flash: 1, spark: 1, smoke: 1 }; // multiplicadores de FX (dev.html game-backed) } this.scene.userData.vmPass = { scene: this.vmScene, camera: this.vmCamera }; @@ -985,6 +985,9 @@ export class Game { // share another (soft smoke) — 1 draw call each, zero per-shot allocation (ring buffer). this.flashFx = new GPUParticles(this.scene, this.camera, { tex: this.flashTex, additive: true }); this.puffFx = new GPUParticles(this.scene, this.camera, { tex: this.puffTex, additive: false }); + // fumaça do cano: nuvem cinza persistente por tiro. _tintFx compartilha uTime/uScale do puff + // (anima sem tick novo) e em quality 'low' devolve o próprio puffFx (degrada sozinho). + this._muzzleSmokeFx = this._tintFx(0x8a8a8a, false, 96); // Muzzle flash (R7.5): 2 SPRITES additivos por tiro — estrela irregular com ruído + // núcleo branco-quente — compactos (0.35-0.5m), na boca do VM (baixo-direita), vida // ≤3 frames (~50ms). Era um cone de 8 segmentos + icosaedro escalado até 1.4 spawnado @@ -1052,8 +1055,8 @@ export class Game { // arma→fallback no _muzzleWorld. Object.assign(this._vmMuzzle, this._vmMuzzleExt || {}); // cápsulas (brass) ejetadas a cada tiro — geo/mat compartilhados, pool reusado - this._casingGeo = new THREE.CylinderGeometry(0.011, 0.011, 0.034, 6); - this._casingMat = new THREE.MeshStandardMaterial({ color: 0xd9a441, metalness: 0.85, roughness: 0.4 }); + this._casingGeo = new THREE.CylinderGeometry(0.016, 0.016, 0.05, 8); + this._casingMat = new THREE.MeshStandardMaterial({ color: 0xd9a441, metalness: 1.0, roughness: 0.35, emissive: 0x3a2a10, emissiveIntensity: 0.4 }); this._casings = []; this._casingPool = []; // granada de fumaça: projétil (mesh) + nuvem de sprites billboard que bloqueia a visão dos bots this._grenades = []; this._smokes = []; @@ -2924,7 +2927,7 @@ export class Game { p.pitch = Math.max(-1.45, Math.min(1.45, p.pitch + vy * REC.perm)); // mesmo clamp do mouse-look p.yaw -= hx * REC.perm; st.last = this.time; - st.sh = Math.min(0.013, st.sh + g * 0.16); + st.sh = Math.min(0.020, st.sh + g * 0.22); } _tryShoot() { const p = this.player, w = WEAPONS[p.weapon]; @@ -3629,6 +3632,18 @@ export class Game { this.flashFx.spawn(pos, { vel: v, life: 0.06 + Math.random() * 0.05, size: fpCls ? 0.07 : 0.11, grow: -0.4 }); } this.puffFx.spawn(pos.clone().addScaledVector(d, 0.18), { vel: d.clone().multiplyScalar(1.2), life: 0.3, size: fpCls ? 0.16 : 0.28, grow: 0.9 }); + // fumaça do cano: 2-3 baforadas lentas subindo/à frente, vida longa, crescendo. Menor e mais + // perto na 1ª pessoa (fpCls) pra não virar blob colado na lente (mesmo cuidado das faíscas, R7.6). + const smokeN = fpCls ? 2 : 3; + const smokeSize = fpCls ? 0.12 : 0.22; + const smU = new THREE.Vector3(0, 1, 0); + for (let i = 0; i < Math.round(smokeN * ((this._fxTune && this._fxTune.smoke) ?? 1)); i++) { + const sv = d.clone().multiplyScalar(0.6 + Math.random() * 0.5) + .addScaledVector(smU, 0.5 + Math.random() * 0.4) + .add(new THREE.Vector3((Math.random() - 0.5) * 0.5, 0, (Math.random() - 0.5) * 0.5)); + this._muzzleSmokeFx.spawn(pos.clone().addScaledVector(d, 0.10 + Math.random() * 0.1), + { vel: sv, life: 0.45 + Math.random() * 0.35, size: smokeSize, grow: 1.6 }); + } } // Boca do cano em WORLD SPACE no instante do tiro: offset local da classe transformada // pelo matrixWorld ATUAL do vm.root (com o kick acumulado) e depois pela câmera — usado @@ -3716,10 +3731,10 @@ export class Game { const right = new THREE.Vector3(1, 0, 0).applyQuaternion(q); const up = new THREE.Vector3(0, 1, 0).applyQuaternion(q); const back = new THREE.Vector3(0, 0, 1).applyQuaternion(q); - c.v.copy(right).multiplyScalar(2.2 + Math.random() * 0.9).addScaledVector(up, 1.7 + Math.random() * 0.6).addScaledVector(back, 0.5 + Math.random() * 0.4); + c.v.copy(right).multiplyScalar(2.2 + Math.random() * 0.9).addScaledVector(up, 2.0 + Math.random() * 0.6).addScaledVector(back, 0.5 + Math.random() * 0.4); c.av.set((Math.random() - 0.5) * 24, (Math.random() - 0.5) * 24, (Math.random() - 0.5) * 24); c.m.rotation.set(Math.random() * 6, Math.random() * 6, Math.random() * 6); - c.m.scale.setScalar(1); c.ttl = 1.6; + c.m.scale.setScalar(1); c.ttl = 2.2; this.scene.add(c.m); this._casings.push(c); } @@ -4909,7 +4924,11 @@ export class Game { camBobLat = Math.sin(p.stepPhase) * 0.009 * amp; } this.camera.position.set(p.pos.x + Math.cos(p.yaw) * camBobLat, p.pos.y + eye + camBobY, p.pos.z - Math.sin(p.yaw) * camBobLat); - this.camera.rotation.set(p.pitch + p.recoilP, p.yaw, 0); + const rp = p.recoilP; // lê o getter 1x (integra/decai o spring do recuo neste frame) + // roll leve por tiro: reusa o mesmo st.sh decadente numa freq. diferente do pitch (61 vs 78) + // → lê como "tremida" e não como punch puro. Vale pra mouse E toque (câmera finaliza só aqui). + const roll = p._rec ? p._rec.sh * Math.sin(this.time * 61) * 0.5 : 0; + this.camera.rotation.set(p.pitch + rp, p.yaw, roll); // footsteps + view bob const moving = sp > 0.6 && p.grounded; if (moving) { From 70fa2e7acda5fa60f280c0a8c913cd9e8801924f Mon Sep 17 00:00:00 2001 From: Emerson Garrido Date: Fri, 21 Aug 2026 16:24:44 -0300 Subject: [PATCH 02/16] =?UTF-8?q?fix(fx):=20fuma=C3=A7a=20do=20cano=20vis?= =?UTF-8?q?=C3=ADvel=20em=20quality:'low'=20(sistema=20cinza=20dedicado)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Investigação com régua no navegador REAL (Edge headed) provou: o caminho de tiro executa sem exceção e RENDERIZA (cápsulas ejetam, muzzle flash dispara, fumaça aparece como nuvem) — não era bug de código nem de cache (URL versionada ?v= de conteúdo serve o código novo). O buraco era só na quality:'low' (perfil da máquina fraca do dono): _tintFx devolvia o puffFx BEGE e minúsculo, então a fumaça sumia justamente onde ele testa. - _muzzleSmokeFx agora é GPUParticles CINZA dedicado (não cai no puff bege em low); 1 draw call, ring buffer 64, compartilha uTime/uScale do puff (anima sem tick). - _flash: baforadas maiores/mais numerosas (fp 3x0.2, mundo 4x0.32) pra ler como fumaça mesmo na low. Nota medida: em mapa branco (Havan) fumaça clara é naturalmente de baixo contraste; manter modesta pra não tapar a própria mira. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Emerson Garrido Agent: Claude Code --- public/js/game.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/public/js/game.js b/public/js/game.js index 528e0919..9abb6f15 100644 --- a/public/js/game.js +++ b/public/js/game.js @@ -987,7 +987,14 @@ export class Game { this.puffFx = new GPUParticles(this.scene, this.camera, { tex: this.puffTex, additive: false }); // fumaça do cano: nuvem cinza persistente por tiro. _tintFx compartilha uTime/uScale do puff // (anima sem tick novo) e em quality 'low' devolve o próprio puffFx (degrada sozinho). - this._muzzleSmokeFx = this._tintFx(0x8a8a8a, false, 96); + // sistema CINZA dedicado — NÃO cai no puffFx bege em quality:'low' (o dono joga em máquina + // fraca, é justamente onde ele quer ver a fumaça). 1 draw call, ring buffer de 64, compartilha + // uTime/uScale do puff (anima sem tick próprio, igual às fx de sangue via _tintFx). + this._muzzleSmokeFx = new GPUParticles(this.scene, this.camera, { tex: this._makeSmokeTex(), additive: false, max: 64 }); + if (this.puffFx && this.puffFx.uniforms) { + this._muzzleSmokeFx.uniforms.uTime = this.puffFx.uniforms.uTime; + this._muzzleSmokeFx.uniforms.uScale = this.puffFx.uniforms.uScale; + } // Muzzle flash (R7.5): 2 SPRITES additivos por tiro — estrela irregular com ruído + // núcleo branco-quente — compactos (0.35-0.5m), na boca do VM (baixo-direita), vida // ≤3 frames (~50ms). Era um cone de 8 segmentos + icosaedro escalado até 1.4 spawnado @@ -3634,8 +3641,8 @@ export class Game { this.puffFx.spawn(pos.clone().addScaledVector(d, 0.18), { vel: d.clone().multiplyScalar(1.2), life: 0.3, size: fpCls ? 0.16 : 0.28, grow: 0.9 }); // fumaça do cano: 2-3 baforadas lentas subindo/à frente, vida longa, crescendo. Menor e mais // perto na 1ª pessoa (fpCls) pra não virar blob colado na lente (mesmo cuidado das faíscas, R7.6). - const smokeN = fpCls ? 2 : 3; - const smokeSize = fpCls ? 0.12 : 0.22; + const smokeN = fpCls ? 3 : 4; + const smokeSize = fpCls ? 0.2 : 0.32; const smU = new THREE.Vector3(0, 1, 0); for (let i = 0; i < Math.round(smokeN * ((this._fxTune && this._fxTune.smoke) ?? 1)); i++) { const sv = d.clone().multiplyScalar(0.6 + Math.random() * 0.5) From 6d803bbfd6b388bed4519a99fba481ba1e960ac4 Mon Sep 17 00:00:00 2001 From: csbrasil-bot Date: Sat, 22 Aug 2026 04:17:51 +0000 Subject: [PATCH 03/16] =?UTF-8?q?Merge=20da=20base=20(conflito=20s=C3=B3?= =?UTF-8?q?=20em=20arquivo=20gerado,=20resolvido=20pelo=20autofix)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent: csbrasil-bot (autofix) Signed-off-by: csbrasil-bot --- package-lock.json | 4 +- package.json | 14 +- scripts/ci/autofix_allowlist.py | 123 ++++++++ scripts/ci/ensure_labels.py | 44 ++- scripts/ci/workflow_security_check.py | 122 +++++++- scripts/fetch-audio.sh | 2 +- scripts/fetch-decals.sh | 2 +- tools/eval/ARCH.md | 402 +++++++++++++------------- tools/eval/armas-check.mjs | 91 ++++++ tools/eval/autofix-check.mjs | 141 +++++++++ tools/eval/deploy-gate-check.mjs | 75 +++++ tools/eval/docs-autoria-check.mjs | 18 +- tools/eval/entrada-check.mjs | 5 +- tools/eval/fixture-check.mjs | 1 - tools/eval/mutacao-cega-check.mjs | 60 ++++ tools/eval/portao-inteiro-check.mjs | 68 +++++ tools/eval/preload-check.mjs | 18 +- tools/eval/redesign-check.mjs | 2 +- tools/eval/replaycam-check.mjs | 5 +- tools/eval/workflow-secret-check.mjs | 74 +++++ 20 files changed, 1043 insertions(+), 228 deletions(-) create mode 100644 scripts/ci/autofix_allowlist.py create mode 100644 tools/eval/armas-check.mjs create mode 100644 tools/eval/autofix-check.mjs create mode 100644 tools/eval/deploy-gate-check.mjs create mode 100644 tools/eval/mutacao-cega-check.mjs create mode 100644 tools/eval/portao-inteiro-check.mjs create mode 100644 tools/eval/workflow-secret-check.mjs diff --git a/package-lock.json b/package-lock.json index 0c5f2a58..e2556455 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "coro-solto", - "version": "2.0.0-alpha.172", + "version": "2.0.0-alpha.176", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "coro-solto", - "version": "2.0.0-alpha.172", + "version": "2.0.0-alpha.176", "license": "AGPL-3.0-only", "dependencies": { "@astrojs/vercel": "^11.0.6", diff --git a/package.json b/package.json index 8eaebaa1..29eebdad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coro-solto", - "version": "2.0.0-alpha.172", + "version": "2.0.0-alpha.176", "license": "AGPL-3.0-only", "description": "CORO SOLTO: Treta Suprema (ex-CS BRASIL) — FPS satírico de navegador com 5 facções brasileiras (site Astro + jogo vanilla em Three.js), gerado em par com agentes de IA (AI generated & AI friendly)", "scripts": { @@ -69,6 +69,7 @@ "eval:pickuparma": "node tools/eval/pickup-arma-check.mjs", "eval:fixture": "node tools/eval/fixture-check.mjs", "eval:preload": "node tools/eval/preload-check.mjs", + "eval:armas": "node tools/eval/armas-check.mjs", "eval:comentario": "node tools/eval/comentario-check.mjs", "eval:posters": "node tools/eval/poster-aspect-check.mjs", "eval:ui": "node tools/eval/ui-check.mjs all", @@ -118,10 +119,15 @@ "eval:regen": "node tools/eval/regen-check.mjs", "eval:ctfwin": "node tools/eval/ctf-win-check.mjs", "eval:spawn": "node tools/eval/spawn-settle-check.mjs", - "check:deploy": "node tools/eval/runner.mjs syntax eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vmlabhud eval:vminspect eval:redesign eval:charvoice eval:screenquery arch:check docs:check media:check menuwalls:check anims:check anims:merge:check travessao:check eval:medianet eval:posters eval:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam", - "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:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam", + "check:deploy": "node tools/eval/runner.mjs syntax eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vmlabhud eval:vminspect eval:redesign eval:charvoice eval:screenquery arch:check docs:check media:check menuwalls:check anims:check anims:merge:check travessao:check eval:medianet eval:posters eval:mutcega eval:autofix eval:deploygate eval:portaointeiro eval:wfsecret eval:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam", + "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", "eval:tags": "node tools/eval/tag-main-check.mjs", - "eval:simclock": "node tools/eval/sim-clock-check.mjs" + "eval:simclock": "node tools/eval/sim-clock-check.mjs", + "eval:mutcega": "node tools/eval/mutacao-cega-check.mjs", + "eval:wfsecret": "node tools/eval/workflow-secret-check.mjs", + "eval:deploygate": "node tools/eval/deploy-gate-check.mjs", + "eval:autofix": "node tools/eval/autofix-check.mjs", + "eval:portaointeiro": "node tools/eval/portao-inteiro-check.mjs" }, "repository": { "type": "git", diff --git a/scripts/ci/autofix_allowlist.py b/scripts/ci/autofix_allowlist.py new file mode 100644 index 00000000..812c7921 --- /dev/null +++ b/scripts/ci/autofix_allowlist.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +"""A trava do autofix: decide se o que o bot mexeu pode virar commit. + +O bot só pode tocar em ARQUIVO GERADO — o que uma ferramenta do repositório +reescreve inteiro a partir do código. Se o conserto encostar em qualquer outro +caminho, o run aborta e comenta em vez de commitar. + +A regra não é conveniência: sem ela, o primeiro conserto errado reescreve o mapa +de um colaborador e ninguém repara. Com ela, o pior caso do bot é regenerar uma +doc que já era derivada. + +Uso: git status --porcelain | python3 scripts/ci/autofix_allowlist.py + python3 scripts/ci/autofix_allowlist.py --selftest +""" +import sys + +# Caminhos que uma ferramenta REGERA por inteiro. Crescer esta lista é decisão de +# dono: cada entrada nova é um arquivo que o bot passa a poder sobrescrever sozinho. +PERMITIDOS = ( + "ARCH.generated.md", + "README.md", + "STATUS.md", + "SCRIPTS.md", + "docs/", + "tools/eval/ARCH.md", +) + +# Caminhos que NUNCA entram, mesmo que um prefixo permitido pareça cobri-los. O +# `.github/` fica de fora porque um bot que edita o próprio workflow que o governa +# consegue ampliar a própria permissão em um commit. +PROIBIDOS = ( + ".github/", + "scripts/", + "supabase/", + "package.json", + "package-lock.json", + "vercel.json", +) + + +def permitido(caminho: str) -> bool: + if any(caminho.startswith(p) for p in PROIBIDOS): + return False + return any(caminho == p or caminho.startswith(p) for p in PERMITIDOS) + + +def separa(caminhos: list[str]) -> tuple[list[str], list[str]]: + ok = [c for c in caminhos if permitido(c)] + fora = [c for c in caminhos if not permitido(c)] + return ok, fora + + +def caminhos_do_porcelain(texto: str) -> list[str]: + """`git status --porcelain` -> lista de caminhos. + + Renomeio vem como `R velho -> novo`; o que importa é o destino, senão um + rename para fora da lista passaria como se fosse o arquivo de origem. + """ + saida = [] + for linha in texto.splitlines(): + if len(linha) < 4: + continue + caminho = linha[3:].strip() + if " -> " in caminho: + caminho = caminho.split(" -> ", 1)[1] + saida.append(caminho.strip('"')) + return saida + + +def selftest() -> int: + casos = [ + ("doc gerada passa", ["ARCH.generated.md"], True), + ("pasta docs passa", ["docs/docs/comecando.md"], True), + ("código do jogo NÃO passa", ["public/js/main.js"], False), + ("mapa de colaborador NÃO passa", ["public/js/map_havan.js"], False), + ("workflow NÃO passa", [".github/workflows/ci.yml"], False), + ("script de CI NÃO passa", ["scripts/ci/pr_classify.py"], False), + ("package.json NÃO passa", ["package.json"], False), + ("uma proibida contamina o lote", ["STATUS.md", "public/js/game.js"], False), + ("nada mexido é lote vazio", [], True), + ("CHANGELOG não é gerado por ferramenta nossa", ["CHANGELOG.md"], False), + ("lote típico de release só toca gerado", ["README.md", "STATUS.md", "docs/docs/comecando.md"], True), + ] + erros = 0 + for nome, caminhos, esperado in casos: + _, fora = separa(caminhos) + obtido = not fora + ok = obtido == esperado + erros += 0 if ok else 1 + print(f"{'ok ' if ok else 'FALHA'} {nome}: fora={fora!r}") + + porcelain = ' M ARCH.generated.md\nR docs/a.md -> public/js/game.js\n?? novo.txt\n' + lidos = caminhos_do_porcelain(porcelain) + ok = lidos == ["ARCH.generated.md", "public/js/game.js", "novo.txt"] + erros += 0 if ok else 1 + print(f"{'ok ' if ok else 'FALHA'} renomeio é julgado pelo DESTINO: {lidos!r}") + + print("selftest verde" if not erros else f"{erros} caso(s) vermelho(s)") + return 0 if not erros else 1 + + +def main() -> int: + if "--selftest" in sys.argv: + return selftest() + # `--caminhos`: a entrada já é uma lista crua (o `git diff --name-only` do merge), + # e não o porcelain. É o modo que o resolvedor de conflito usa. + if "--caminhos" in sys.argv: + caminhos = [l.strip() for l in sys.stdin.read().splitlines() if l.strip()] + else: + caminhos = caminhos_do_porcelain(sys.stdin.read()) + ok, fora = separa(caminhos) + if fora: + print("BLOQUEADO: o conserto encostou em arquivo que o bot não pode reescrever:") + for c in fora: + print(f" - {c}") + return 1 + for c in ok: + print(c) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/ci/ensure_labels.py b/scripts/ci/ensure_labels.py index 6810e02e..e54053b2 100644 --- a/scripts/ci/ensure_labels.py +++ b/scripts/ci/ensure_labels.py @@ -17,18 +17,50 @@ "crash-auto": ("B60205", "crash de produção reportado por /api/jserror ou prod-watch"), "stale-backlog": ("c2e0c6", "Issue antiga ou desalinhada com o checkout atual; precisa revalidação"), "needs-repro": ("f9d0c4", "Issue precisa passos de reprodução mais concretos"), + "preview-autorizado": ("0e8a16", "Mantenedor revisou o SHA atual e liberou preview do fork na Vercel"), + "pronto-pra-merge": ("0e8a16", "Portões verdes e diff dentro do aceito: falta só o merge, que é humano"), } -def main() -> int: - for label in sys.argv[1:]: +def comandos(nomes: list[str]) -> list[list[str]]: + """Os `gh label create` que ESTE pedido produz. + + Nome fora do dicionário some em silêncio, e foi assim que `preview-autorizado` + passou meses sendo pedida por um workflow sem nunca ser criada — o job de preview + de fork inteiro virou código morto atrás de um rótulo que ninguém podia aplicar. + """ + out = [] + for label in nomes: if label not in LABELS: continue color, description = LABELS[label] - subprocess.run( - ["gh", "label", "create", label, "--color", color, "--description", description, "--force"], - check=True, - ) + out.append(["gh", "label", "create", label, "--color", color, "--description", description, "--force"]) + return out + + +def selftest() -> int: + casos = [ + ("rótulo conhecido vira um comando", ["safe-automerge"], ["safe-automerge"]), + ("desconhecido é ignorado", ["nao-existe"], []), + ("conhecido junto de desconhecido não contamina", ["nao-existe", "bot-fixable"], ["bot-fixable"]), + ("lista vazia não faz nada", [], []), + ("preview-autorizado está registrado", ["preview-autorizado"], ["preview-autorizado"]), + ] + erros = 0 + for nome, pedido, esperado in casos: + obtido = [c[3] for c in comandos(pedido)] + ok = obtido == esperado + erros += 0 if ok else 1 + print(f"{'ok ' if ok else 'FALHA'} {nome}: {obtido!r}") + print("selftest verde" if not erros else f"{erros} caso(s) vermelho(s)") + return 0 if not erros else 1 + + +def main() -> int: + if "--selftest" in sys.argv: + return selftest() + for comando in comandos(sys.argv[1:]): + subprocess.run(comando, check=True) return 0 diff --git a/scripts/ci/workflow_security_check.py b/scripts/ci/workflow_security_check.py index 1740ca9f..53baec24 100644 --- a/scripts/ci/workflow_security_check.py +++ b/scripts/ci/workflow_security_check.py @@ -9,6 +9,16 @@ def preview_failures(source: str) -> list[str]: + """Contrato do preview de fork POR APROVAÇÃO (desenho antigo). + + Vale enquanto o `preview-bot.yml` tiver o job que publica: ele roda `vercel build`, + isto é, executa o build DO FORK com o token no ambiente, e por isso depende de um + mantenedor revisar o SHA. Quando o preview passou a ser build-sem-segredo + + deploy-sem-código (ver `separacao_failures`), este job deixou de ser necessário — + mas enquanto existir, ele tem de manter TODAS as travas. + """ + if 'vercel deploy' not in source: + return [] # o job de publicação saiu do arquivo: nada a guardar aqui errors = [] required = { 'types: [opened, synchronize, reopened, labeled]': 'evento labeled ausente', @@ -27,13 +37,84 @@ def preview_failures(source: str) -> list[str]: for marker, message in required.items(): if marker not in source: errors.append(message) - if '--add-label "preview-autorizado"' in source: + if '--add-label' in source and 'preview-autorizado' in source: errors.append('workflow autoaprova código de fork') if 'preview_autorizado=true' in source: errors.append('workflow decide autorização sem mantenedor') return errors +def separacao_failures(build: str, deploy: str) -> list[str]: + """Contrato do preview de fork POR SEPARAÇÃO (desenho de 22/08). + + A aprovação manual existia porque UM job fazia as duas coisas: rodava o build do + fork E tinha o token. Separando, os dois lados ficam seguros sozinhos e ninguém + precisa clicar: + + PRV1 quem COMPILA roda código do fork e não recebe segredo — `pull_request` (não + `pull_request_target`), e nenhuma referência a `secrets.` no arquivo; + PRV2 quem PUBLICA roda no contexto base (`workflow_run`), que é o que lhe dá o + segredo mesmo vindo de fork; + PRV4 quem PUBLICA não interpola `${{ }}` dentro de `run:`. A expressão é substituída + no TEXTO do script antes do shell existir, então valor com aspas ou `$(...)` vira + comando — e no caminho de deploy os valores vêm de fora (o número do PR atravessa + um artefato escrito por job que rodou código do fork, a URL é saída de comando). + O CodeQL pegou exatamente isso aqui, como injeção crítica, antes do merge. + PRV3 quem PUBLICA não executa NADA do PR: sem checkout da branch do fork, sem + `npm ci`, sem `npm run`, e o deploy é `--prebuilt` (só envia arquivo). + Furar isto devolve o token para as mãos de quem abriu o PR. + """ + errors = [] + if not build or not deploy: + return ['preview por separação incompleto: falta preview-build.yml ou preview-deploy.yml'] + + # Só a INSTRUÇÃO conta. Os dois arquivos explicam em comentário o que NÃO fazem + # ("não roda npm ci", "não usa pull_request_target") e ler o comentário como se + # fosse código acusaria justamente quem documentou a trava. + def codigo(texto: str) -> str: + return '\n'.join(l for l in texto.splitlines() if not l.lstrip().startswith('#')) + + build, deploy = codigo(build), codigo(deploy) + + if 'pull_request_target' in build: + errors.append('PRV1 o job que COMPILA usa pull_request_target — passaria a enxergar segredo rodando código do fork') + if 'pull_request:' not in build: + errors.append('PRV1 o job que COMPILA não roda em pull_request') + if 'secrets.' in build: + errors.append('PRV1 o job que COMPILA referencia `secrets.` — ele roda código do fork e não pode ter o que roubar') + + if 'workflow_run:' not in deploy: + errors.append('PRV2 o job que PUBLICA não roda em workflow_run — sem contexto base não há segredo em PR de fork') + if 'secrets.VERCEL_TOKEN' not in deploy: + errors.append('PRV2 o job que PUBLICA não usa o token da Vercel') + + # PRV4: `${{ }}` só pode aparecer em `env:`/`with:`/`if:`, nunca dentro do script. + dentro_de_run = False + for linha in deploy.splitlines(): + despido = linha.strip() + if re.match(r'run:\s*\|', despido): + dentro_de_run = True + continue + if dentro_de_run: + # o bloco acaba quando a indentação volta para o nível da chave do passo + if despido and not linha.startswith(' '): + dentro_de_run = False + elif '${{' in linha: + errors.append(f'PRV4 `{despido[:60]}` interpola expressão dentro de run: — passe por env:') + + if '--prebuilt' not in deploy: + errors.append('PRV3 o deploy não é --prebuilt: estaria construindo, e construir é executar código do PR') + for proibido, motivo in ( + ('actions/checkout', 'faz checkout — traria código do PR para o job que tem o token'), + ('npm ci', 'roda npm ci — executaria script do PR'), + ('npm run', 'roda npm run — executaria script do PR'), + ('vercel build', 'roda vercel build — é o build do fork com o token no ambiente'), + ): + if proibido in deploy: + errors.append(f'PRV3 o job que PUBLICA {motivo}') + return errors + + def supply_failures(workflows: dict[Path, str]) -> list[str]: errors = [] for path, source in workflows.items(): @@ -54,7 +135,26 @@ def read_workflows(root: Path = Path('.github/workflows')) -> dict[Path, str]: } +BUILD = Path('.github/workflows/preview-build.yml') +DEPLOY = Path('.github/workflows/preview-deploy.yml') + + +def _ler(p: Path) -> str: + return p.read_text(encoding='utf-8') if p.exists() else '' + + def selftest(source: str) -> list[str]: + build, deploy = _ler(BUILD), _ler(DEPLOY) + separacao = { + 'compila-com-segredo': (build + '\n env:\n X: ${{ secrets.VERCEL_TOKEN }}\n', deploy), + 'compila-com-target': (build.replace(' pull_request:', ' pull_request_target:'), deploy), + 'publica-sem-run': (build, deploy.replace(' workflow_run:', ' schedule:')), + 'publica-faz-checkout': (build, deploy + '\n - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683\n'), + 'publica-constroi': (build, deploy.replace('--prebuilt', '')), + 'publica-interpola': (build, deploy.replace('gh pr comment "$PR_NUM"', 'gh pr comment "${{ steps.pr.outputs.numero }}"')), + } + missed_sep = [n for n, (b, d) in separacao.items() if not separacao_failures(b, d)] + mutations = { 'auto-label': source + '\n# --add-label "preview-autorizado"\n', 'sem-ator': source.replace('repos/$REPO/collaborators/$ACTOR/permission', 'repos/$REPO'), @@ -65,10 +165,22 @@ def selftest(source: str) -> list[str]: 'action-mutável': re.sub(r'actions/checkout@[0-9a-f]{40}', 'actions/checkout@v4', source), 'cli-mutável': re.sub(r'vercel@\d+\.\d+\.\d+', 'vercel@latest', source), } + # As mutações acima descrevem o contrato ANTIGO (preview por aprovação). Quando o + # job que publicava sai do arquivo, elas deixam de ter alvo — testá-las ali seria + # exigir mordida de uma régua que não tem mais o que guardar. O contrato novo é + # medido pelas mutações de `separacao` logo acima. + antigo_vivo = 'vercel deploy' in source missed = [ name for name, mutated in mutations.items() - if not (preview_failures(mutated) + supply_failures({WORKFLOW: mutated})) + if antigo_vivo and not (preview_failures(mutated) + supply_failures({WORKFLOW: mutated})) ] + # Fornecimento (action e CLI presas) vale sempre, e o alvo passou a ser os dois + # arquivos novos: um tem as actions, o outro tem a CLI da Vercel. + fornecimento = { + 'action-mutável': (BUILD, re.sub(r'actions/checkout@[0-9a-f]{40}', 'actions/checkout@v4', build)), + 'cli-mutável': (DEPLOY, re.sub(r'vercel@\d+\.\d+\.\d+', 'vercel@latest', deploy)), + } + missed += [n for n, (alvo, mutado) in fornecimento.items() if not supply_failures({alvo: mutado})] with tempfile.TemporaryDirectory() as tmp: mutant = Path(tmp) / 'mutable-action.yaml' mutant.write_text('steps:\\n - uses: actions/checkout@v4\\n', encoding='utf-8') @@ -83,12 +195,14 @@ def main() -> int: args = parser.parse_args() source = WORKFLOW.read_text(encoding='utf-8') workflows = read_workflows() - errors = preview_failures(source) + supply_failures(workflows) + errors = (preview_failures(source) + + separacao_failures(_ler(BUILD), _ler(DEPLOY)) + + supply_failures(workflows)) if errors: for error in errors: print(f'WFS FAIL: {error}') return 1 - print('WFS PASS: preview de fork exige aprovação manual presa ao SHA') + print('WFS PASS: quem compila código de fork não tem segredo; quem tem segredo não executa código de fork') if args.selftest: missed = selftest(source) if missed: diff --git a/scripts/fetch-audio.sh b/scripts/fetch-audio.sh index 58d27b67..3dd9e6b2 100755 --- a/scripts/fetch-audio.sh +++ b/scripts/fetch-audio.sh @@ -18,7 +18,7 @@ if [ -f "$DEST/manifest.json" ]; then fi mkdir -p "$DEST" echo "Baixando pacote de áudio de: $URL" -curl -fsSL "$URL" -o /tmp/csbrasil-audio.zip +curl --retry 5 --retry-delay 3 --retry-all-errors --connect-timeout 20 -fsSL "$URL" -o /tmp/csbrasil-audio.zip unzip -o -q /tmp/csbrasil-audio.zip -d "$DEST/" [ -f "$DEST/manifest.json" ] || cp "$DEST/manifest.example.json" "$DEST/manifest.json" echo "Pronto. Áudio instalado em $DEST/." diff --git a/scripts/fetch-decals.sh b/scripts/fetch-decals.sh index b9697f17..dcafbcf8 100755 --- a/scripts/fetch-decals.sh +++ b/scripts/fetch-decals.sh @@ -24,6 +24,6 @@ if [ -d "$DEST" ] && [ "$(ls -1 "$DEST"/*.png 2>/dev/null | grep -cv '/or-')" -g fi mkdir -p "$DEST" echo "Baixando decalques de: $URL" -curl -fsSL "$URL" -o /tmp/csbrasil-decals.zip +curl --retry 5 --retry-delay 3 --retry-all-errors --connect-timeout 20 -fsSL "$URL" -o /tmp/csbrasil-decals.zip unzip -o -q /tmp/csbrasil-decals.zip -d "$DEST/" echo "Pronto. $(ls -1 "$DEST"/*.png | wc -l | tr -d ' ') decalques em $DEST/." diff --git a/tools/eval/ARCH.md b/tools/eval/ARCH.md index 2f09a271..e8d932ec 100644 --- a/tools/eval/ARCH.md +++ b/tools/eval/ARCH.md @@ -3,41 +3,41 @@ > Gerado por `node tools/gen-arch.mjs`. **Não edite este bloco à mão.** -> Versão do jogo: 2.0.0-alpha.172 · `npm run arch` para regenerar · `npm run arch:check` no CI. +> Versão do jogo: 2.0.0-alpha.176 · `npm run arch` para regenerar · `npm run arch:check` no CI. ## Tamanho dos arquivos indexados | Arquivo | Linhas | Símbolos | |---|---:|---:| -| `public/js/game.js` | 6895 | 249 | -| `public/js/main.js` | 2681 | 252 | -| `public/js/glbchars.js` | 838 | 60 | +| `public/js/game.js` | 6911 | 251 | +| `public/js/main.js` | 2699 | 252 | +| `public/js/glbchars.js` | 845 | 60 | | `public/js/characters.js` | 1069 | 40 | | `public/js/vmattach.js` | 629 | 4 | | `public/js/springs.js` | 261 | 28 | -| `public/js/weapons.js` | 345 | 20 | +| `public/js/weapons.js` | 347 | 20 | ## Maiores métodos de `game.js` — onde o conflito mora -Os 15 maiores somam **3101 linhas (45% do arquivo)**. Método grande = PR irrevisável e merge conflitante. +Os 15 maiores somam **3105 linhas (45% do arquivo)**. Método grande = PR irrevisável e merge conflitante. | Linhas | Início | Método | | |---:|---:|---|---| -| 812 | 5546 | `_updateBot()` | ⚠️ candidato a extração | -| 538 | 572 | `constructor()` | 🔴 append-only | -| 315 | 4779 | `_updatePlayer()` | ⚠️ candidato a extração | -| 248 | 2141 | `_resetPositions()` | | -| 241 | 1207 | `_buildViewModels()` | | -| 148 | 5094 | `_updatePickups()` | | -| 133 | 4356 | `_botCtf()` | | -| 115 | 1885 | `_touchControls()` | | -| 84 | 4090 | `_initCTF()` | | -| 83 | 2929 | `_tryShoot()` | | -| 80 | 6702 | `_updateHud()` | | -| 79 | 3290 | `_dmgArc()` | | -| 76 | 4495 | `_updateCtfHud()` | | -| 76 | 6782 | `update()` | 🔴 append-only | -| 73 | 3217 | `_kill()` | | +| 812 | 5562 | `_updateBot()` | ⚠️ candidato a extração | +| 542 | 586 | `constructor()` | 🔴 append-only | +| 315 | 4797 | `_updatePlayer()` | ⚠️ candidato a extração | +| 248 | 2159 | `_resetPositions()` | | +| 241 | 1225 | `_buildViewModels()` | | +| 148 | 5112 | `_updatePickups()` | | +| 133 | 4374 | `_botCtf()` | | +| 115 | 1903 | `_touchControls()` | | +| 84 | 4108 | `_initCTF()` | | +| 83 | 2947 | `_tryShoot()` | | +| 80 | 6718 | `_updateHud()` | | +| 79 | 3308 | `_dmgArc()` | | +| 76 | 4513 | `_updateCtfHud()` | | +| 76 | 6798 | `update()` | 🔴 append-only | +| 73 | 3235 | `_kill()` | | ## Tabela de CONFLITO — resolvida para as linhas de hoje @@ -47,20 +47,20 @@ faixas disjuntas simultaneamente com zero conflito de conteúdo. | Frente | Faixas em `game.js` | Arquivos exclusivos | |---|---|---| -| **ARMAS / VIEWMODEL** | `304–306` `335–429` `456–477` `1207–1605` `2694–2699` `2781–2864` `2883–3076` `3511–3534` `3582–3644` `3711–3727` | `public/js/vmattach.js` `public/js/springs.js` `public/js/weapons.js` `public/js/fparms.js` `public/js/handik.js` `public/js/recoil.js` `public/js/vmlab.js` | -| **BOTS / JOGABILIDADE** | `158–161` `212–212` `238–249` `519–530` `3178–3289` `4034–4089` `4252–4488` `4571–4593` `4779–5093` `5418–5435` `5517–6357` | — | -| **MAPAS / MUNDO** | `1153–1206` `2141–2388` `4090–4229` `5094–5241` | `public/js/maps.js` `public/js/mapprops.js` `public/js/map_brasilia.js` `public/js/map_havan.js` `public/js/map_piscina.js` `public/js/map_piscinao_ramos.js` `public/js/map_ferrovelho.js` | -| **GRÁFICOS / FX** | `1606–1615` `1730–1761` `2624–2636` `3535–3573` `3655–3710` | `public/js/bloom.js` `public/js/textures.js` `public/js/vao.js` `public/js/stylize.js` `public/js/gpuparticles.js` | -| **UI / HUD / MENU** | `1110–1152` `2581–2602` `2618–2623` `2637–2643` `3290–3431` `3447–3510` `6525–6588` `6619–6666` `6702–6781` | `public/js/main.js` `public/style.css` `src/pages/index.astro` | +| **ARMAS / VIEWMODEL** | `304–306` `335–429` `456–477` `1225–1623` `2712–2717` `2799–2882` `2901–3094` `3529–3552` `3600–3662` `3729–3745` | `public/js/vmattach.js` `public/js/springs.js` `public/js/weapons.js` `public/js/fparms.js` `public/js/handik.js` `public/js/recoil.js` `public/js/vmlab.js` | +| **BOTS / JOGABILIDADE** | `158–161` `212–212` `238–249` `519–530` `3196–3307` `4052–4107` `4270–4506` `4589–4611` `4797–5111` `5434–5451` `5533–6373` | — | +| **MAPAS / MUNDO** | `1171–1224` `2159–2406` `4108–4247` `5112–5259` | `public/js/maps.js` `public/js/mapprops.js` `public/js/map_brasilia.js` `public/js/map_havan.js` `public/js/map_piscina.js` `public/js/map_piscinao_ramos.js` `public/js/map_ferrovelho.js` | +| **GRÁFICOS / FX** | `1624–1633` `1748–1779` `2642–2654` `3553–3591` `3673–3728` | `public/js/bloom.js` `public/js/textures.js` `public/js/vao.js` `public/js/stylize.js` `public/js/gpuparticles.js` | +| **UI / HUD / MENU** | `1128–1170` `2599–2620` `2636–2641` `2655–2661` `3308–3449` `3465–3528` `6541–6604` `6635–6682` `6718–6797` | `public/js/main.js` `public/style.css` `src/pages/index.astro` | | **ÁUDIO** | — | `public/js/audio.js` | | **PERSONAGENS** | — | `public/js/characters.js` `public/js/glbchars.js` | | **SITE / BACKEND** | — | `src/` `supabase/` | -**🔴 Zonas vermelhas (append-only, qualquer frente pode precisar):** `update()` 6782–6857 · `_dom()` 1110–1152 · `constructor()` 572–1109 +**🔴 Zonas vermelhas (append-only, qualquer frente pode precisar):** `update()` 6798–6873 · `_dom()` 1128–1170 · `constructor()` 586–1127 Nenhuma sobreposição entre frentes — todas as faixas são disjuntas. ✓ -Cobertura: **3728 de 6895 linhas (54%)** do `game.js` têm dono declarado. O resto é território neutro — declare a frente mesmo assim. +Cobertura: **3728 de 6911 linhas (54%)** do `game.js` têm dono declarado. O resto é território neutro — declare a frente mesmo assim.
Índice completo de game.js (todos os símbolos) @@ -105,178 +105,180 @@ Cobertura: **3728 de 6895 linhas (54%)** do `game.js` têm dono declarado. O res | 543 | `botTier` | 4 | | 547 | `_cyclePool` | 4 | | 551 | `_rosterPool` | 12 | -| 563 | `pickMatchRoster` | 8 | -| 572 | `constructor()` | 538 | -| 1110 | `_dom()` | 43 | -| 1153 | `_buildEnv()` | 54 | -| 1207 | `_buildViewModels()` | 241 | -| 1448 | `_vmFrame` | 158 | -| 1606 | `_makePuffTexture()` | 10 | -| 1616 | `_makeBloodTex()` | 19 | -| 1635 | `_makeBloodPoolTex()` | 21 | -| 1656 | `_bloodDecal()` | 16 | -| 1672 | `_makeBloodFx()` | 20 | -| 1692 | `_bloodSpatter()` | 18 | -| 1710 | `_bloodPoolAt()` | 6 | -| 1716 | `_updateBlood()` | 14 | -| 1730 | `_makeFlashTex()` | 22 | -| 1752 | `_makeFlashCoreTex()` | 10 | -| 1762 | `_input()` | 2 | -| 1764 | `_kd` | 42 | -| 1806 | `_ku` | 4 | -| 1810 | `_md` | 34 | -| 1844 | `_mu` | 7 | -| 1851 | `_mm` | 15 | -| 1866 | `_cc` | 1 | -| 1867 | `_blur` | 1 | -| 1868 | `_plc` | 17 | -| 1885 | `_touchControls()` | 115 | -| 2000 | `_aimAssist()` | 28 | -| 2028 | `_requestLock()` | 24 | -| 2052 | `_travaAtalhos()` | 4 | -| 2056 | `_soltaAtalhos()` | 3 | -| 2059 | `_acceptInput()` | 8 | -| 2067 | `_pauseBackdrop()` | 7 | -| 2074 | `_radioShow()` | 6 | -| 2080 | `_radioUi()` | 8 | -| 2088 | `_radioPick()` | 14 | -| 2102 | `start()` | 4 | -| 2106 | `_startRound()` | 35 | -| 2141 | `_resetPositions()` | 248 | -| 2389 | `_checkCtfAlvo()` | 13 | -| 2402 | `_checkPace()` | 13 | -| 2415 | `_endRound()` | 37 | -| 2452 | `_fimDaPartida()` | 7 | -| 2459 | `_endMatch()` | 58 | -| 2517 | `_ensureDolly()` | 41 | -| 2558 | `_tickDolly()` | 23 | -| 2581 | `setPaused()` | 22 | -| 2603 | `_now()` | 3 | -| 2606 | `pauseArmed()` | 1 | -| 2607 | `_syncPauseArm()` | 7 | -| 2614 | `resume()` | 4 | -| 2618 | `applySettings()` | 6 | -| 2624 | `_applyQuality()` | 13 | -| 2637 | `onResize()` | 7 | -| 2644 | `_switchTeam()` | 50 | -| 2694 | `_applyVmVisibility()` | 6 | -| 2700 | `_vmlabEnsure()` | 14 | -| 2714 | `_vmlabFrame()` | 28 | -| 2742 | `_tuneGet()` | 15 | -| 2757 | `_tune()` | 23 | -| 2780 | `_fxSet()` | 1 | -| 2781 | `_switchWeapon()` | 32 | -| 2813 | `_deploySfx()` | 7 | -| 2820 | `_scope()` | 17 | -| 2837 | `_zoomFov()` | 8 | -| 2845 | `_reloading()` | 1 | -| 2846 | `_startReload()` | 19 | -| 2865 | `_reloadLayers()` | 18 | -| 2883 | `_installRecoil()` | 33 | -| 2916 | `_shotRecoil()` | 13 | -| 2929 | `_tryShoot()` | 83 | -| 3012 | `_meleeHit()` | 12 | -| 3024 | `_fireHitscan()` | 53 | -| 3077 | `_surfaceOf()` | 27 | -| 3104 | `_fleshImpact()` | 35 | -| 3139 | `_fxVoice()` | 9 | -| 3148 | `_impactSfx()` | 14 | -| 3162 | `_tintFx()` | 16 | -| 3178 | `_damage()` | 39 | -| 3217 | `_kill()` | 73 | -| 3290 | `_dmgArc()` | 79 | -| 3369 | `_mkBanner()` | 9 | -| 3378 | `_hitmarker()` | 15 | -| 3393 | `_dmgNumber()` | 20 | -| 3413 | `_feed()` | 19 | -| 3432 | `_skullIcon()` | 6 | -| 3438 | `_killfeedWeaponIcon()` | 9 | -| 3447 | `_wpnIcon()` | 64 | -| 3511 | `_tracer()` | 24 | -| 3535 | `_puff()` | 39 | -| 3574 | `_holeDecalMat()` | 8 | -| 3582 | `_flash()` | 54 | -| 3636 | `_muzzleWorld()` | 9 | -| 3645 | `_updateDoors()` | 10 | -| 3655 | `_updateFx()` | 56 | -| 3711 | `_ejectCasing()` | 17 | -| 3728 | `_makeCtfFlagTex()` | 23 | -| 3751 | `_paintFlagSymbol()` | 9 | -| 3760 | `_flagTexFor()` | 26 | -| 3786 | `_legadoSimbolo()` | 8 | -| 3794 | `_loadCtfSymbols()` | 22 | -| 3816 | `_makeCtfZoneTex()` | 31 | -| 3847 | `_makeSmokeTex()` | 8 | -| 3855 | `_updateSmokeHud()` | 6 | -| 3861 | `_spawnGrenade()` | 11 | -| 3872 | `_throwSmoke()` | 8 | -| 3880 | `_throwFrag()` | 10 | -| 3890 | `_explodeFrag()` | 38 | -| 3928 | `_corDaFumaca()` | 15 | -| 3943 | `_popSmoke()` | 19 | -| 3962 | `_updateGrenades()` | 27 | -| 3989 | `_teamColor()` | 14 | -| 4003 | `_teamInk()` | 6 | -| 4009 | `_factionOf()` | 1 | -| 4010 | `_voiceKey()` | 1 | -| 4011 | `_teamName()` | 1 | -| 4012 | `_teamTag()` | 6 | -| 4018 | `_plaqueta()` | 13 | -| 4031 | `_mirror()` | 3 | -| 4034 | `_botSeparation()` | 56 | -| 4090 | `_initCTF()` | 84 | -| 4174 | `_updateCTF()` | 56 | -| 4230 | `_ctfWin()` | 22 | -| 4252 | `_freeYaw()` | 25 | -| 4277 | `_pullString()` | 23 | -| 4300 | `_walkReach()` | 18 | -| 4318 | `_wpComp()` | 16 | -| 4334 | `_findPathLocal()` | 22 | -| 4356 | `_botCtf()` | 133 | -| 4489 | `_hideCtfHud()` | 6 | -| 4495 | `_updateCtfHud()` | 76 | -| 4571 | `_collide()` | 23 | -| 4594 | `_collideRot()` | 26 | -| 4620 | `_freeSpot()` | 30 | -| 4650 | `_retaAndavel()` | 20 | -| 4670 | `_walkDepth()` | 16 | -| 4686 | `_noteHit()` | 17 | -| 4703 | `_deathFeedback()` | 43 | -| 4746 | `_updateReplayCam()` | 33 | -| 4779 | `_updatePlayer()` | 315 | -| 5094 | `_updatePickups()` | 148 | -| 5242 | `_wpnMode()` | 5 | -| 5247 | `_botWeapon()` | 12 | -| 5259 | `_municaoInfinita()` | 1 | -| 5260 | `_pickupAllowed()` | 7 | -| 5267 | `_grabPickup()` | 35 | -| 5302 | `_assentarNoChao()` | 11 | -| 5313 | `_dropWeapon()` | 18 | -| 5331 | `_sumirDrop()` | 36 | -| 5367 | `_spawnY()` | 3 | -| 5370 | `_pickSpawn()` | 23 | -| 5393 | `_respawnPlayer()` | 25 | -| 5418 | `_losClear()` | 18 | -| 5436 | `_botCall()` | 37 | -| 5473 | `_teamMarkTex()` | 23 | -| 5496 | `_makeTeamMark()` | 14 | -| 5510 | `_updateTeamMark()` | 7 | -| 5517 | `_botEye()` | 1 | -| 5518 | `_enemyOf()` | 8 | -| 5526 | `_duelToken()` | 20 | -| 5546 | `_updateBot()` | 812 | -| 6358 | `_flushTraining()` | 13 | -| 6371 | `_updateBotNN()` | 71 | -| 6442 | `_botShootNN()` | 45 | -| 6487 | `_radarFoot()` | 38 | -| 6525 | `_updateRadar()` | 64 | -| 6589 | `_banner()` | 26 | -| 6615 | `_resultadoDaRodada()` | 4 | -| 6619 | `_showScoreboard()` | 48 | -| 6667 | `_updateWeaponHud()` | 35 | -| 6702 | `_updateHud()` | 80 | -| 6782 | `update()` | 76 | -| 6858 | `dispose()` | 37 | +| 563 | `pickMatchRoster` | 10 | +| 573 | `BOT_WEAPON_POOL` | 5 | +| 578 | `pickMatchWeapons` | 7 | +| 586 | `constructor()` | 542 | +| 1128 | `_dom()` | 43 | +| 1171 | `_buildEnv()` | 54 | +| 1225 | `_buildViewModels()` | 241 | +| 1466 | `_vmFrame` | 158 | +| 1624 | `_makePuffTexture()` | 10 | +| 1634 | `_makeBloodTex()` | 19 | +| 1653 | `_makeBloodPoolTex()` | 21 | +| 1674 | `_bloodDecal()` | 16 | +| 1690 | `_makeBloodFx()` | 20 | +| 1710 | `_bloodSpatter()` | 18 | +| 1728 | `_bloodPoolAt()` | 6 | +| 1734 | `_updateBlood()` | 14 | +| 1748 | `_makeFlashTex()` | 22 | +| 1770 | `_makeFlashCoreTex()` | 10 | +| 1780 | `_input()` | 2 | +| 1782 | `_kd` | 42 | +| 1824 | `_ku` | 4 | +| 1828 | `_md` | 34 | +| 1862 | `_mu` | 7 | +| 1869 | `_mm` | 15 | +| 1884 | `_cc` | 1 | +| 1885 | `_blur` | 1 | +| 1886 | `_plc` | 17 | +| 1903 | `_touchControls()` | 115 | +| 2018 | `_aimAssist()` | 28 | +| 2046 | `_requestLock()` | 24 | +| 2070 | `_travaAtalhos()` | 4 | +| 2074 | `_soltaAtalhos()` | 3 | +| 2077 | `_acceptInput()` | 8 | +| 2085 | `_pauseBackdrop()` | 7 | +| 2092 | `_radioShow()` | 6 | +| 2098 | `_radioUi()` | 8 | +| 2106 | `_radioPick()` | 14 | +| 2120 | `start()` | 4 | +| 2124 | `_startRound()` | 35 | +| 2159 | `_resetPositions()` | 248 | +| 2407 | `_checkCtfAlvo()` | 13 | +| 2420 | `_checkPace()` | 13 | +| 2433 | `_endRound()` | 37 | +| 2470 | `_fimDaPartida()` | 7 | +| 2477 | `_endMatch()` | 58 | +| 2535 | `_ensureDolly()` | 41 | +| 2576 | `_tickDolly()` | 23 | +| 2599 | `setPaused()` | 22 | +| 2621 | `_now()` | 3 | +| 2624 | `pauseArmed()` | 1 | +| 2625 | `_syncPauseArm()` | 7 | +| 2632 | `resume()` | 4 | +| 2636 | `applySettings()` | 6 | +| 2642 | `_applyQuality()` | 13 | +| 2655 | `onResize()` | 7 | +| 2662 | `_switchTeam()` | 50 | +| 2712 | `_applyVmVisibility()` | 6 | +| 2718 | `_vmlabEnsure()` | 14 | +| 2732 | `_vmlabFrame()` | 28 | +| 2760 | `_tuneGet()` | 15 | +| 2775 | `_tune()` | 23 | +| 2798 | `_fxSet()` | 1 | +| 2799 | `_switchWeapon()` | 32 | +| 2831 | `_deploySfx()` | 7 | +| 2838 | `_scope()` | 17 | +| 2855 | `_zoomFov()` | 8 | +| 2863 | `_reloading()` | 1 | +| 2864 | `_startReload()` | 19 | +| 2883 | `_reloadLayers()` | 18 | +| 2901 | `_installRecoil()` | 33 | +| 2934 | `_shotRecoil()` | 13 | +| 2947 | `_tryShoot()` | 83 | +| 3030 | `_meleeHit()` | 12 | +| 3042 | `_fireHitscan()` | 53 | +| 3095 | `_surfaceOf()` | 27 | +| 3122 | `_fleshImpact()` | 35 | +| 3157 | `_fxVoice()` | 9 | +| 3166 | `_impactSfx()` | 14 | +| 3180 | `_tintFx()` | 16 | +| 3196 | `_damage()` | 39 | +| 3235 | `_kill()` | 73 | +| 3308 | `_dmgArc()` | 79 | +| 3387 | `_mkBanner()` | 9 | +| 3396 | `_hitmarker()` | 15 | +| 3411 | `_dmgNumber()` | 20 | +| 3431 | `_feed()` | 19 | +| 3450 | `_skullIcon()` | 6 | +| 3456 | `_killfeedWeaponIcon()` | 9 | +| 3465 | `_wpnIcon()` | 64 | +| 3529 | `_tracer()` | 24 | +| 3553 | `_puff()` | 39 | +| 3592 | `_holeDecalMat()` | 8 | +| 3600 | `_flash()` | 54 | +| 3654 | `_muzzleWorld()` | 9 | +| 3663 | `_updateDoors()` | 10 | +| 3673 | `_updateFx()` | 56 | +| 3729 | `_ejectCasing()` | 17 | +| 3746 | `_makeCtfFlagTex()` | 23 | +| 3769 | `_paintFlagSymbol()` | 9 | +| 3778 | `_flagTexFor()` | 26 | +| 3804 | `_legadoSimbolo()` | 8 | +| 3812 | `_loadCtfSymbols()` | 22 | +| 3834 | `_makeCtfZoneTex()` | 31 | +| 3865 | `_makeSmokeTex()` | 8 | +| 3873 | `_updateSmokeHud()` | 6 | +| 3879 | `_spawnGrenade()` | 11 | +| 3890 | `_throwSmoke()` | 8 | +| 3898 | `_throwFrag()` | 10 | +| 3908 | `_explodeFrag()` | 38 | +| 3946 | `_corDaFumaca()` | 15 | +| 3961 | `_popSmoke()` | 19 | +| 3980 | `_updateGrenades()` | 27 | +| 4007 | `_teamColor()` | 14 | +| 4021 | `_teamInk()` | 6 | +| 4027 | `_factionOf()` | 1 | +| 4028 | `_voiceKey()` | 1 | +| 4029 | `_teamName()` | 1 | +| 4030 | `_teamTag()` | 6 | +| 4036 | `_plaqueta()` | 13 | +| 4049 | `_mirror()` | 3 | +| 4052 | `_botSeparation()` | 56 | +| 4108 | `_initCTF()` | 84 | +| 4192 | `_updateCTF()` | 56 | +| 4248 | `_ctfWin()` | 22 | +| 4270 | `_freeYaw()` | 25 | +| 4295 | `_pullString()` | 23 | +| 4318 | `_walkReach()` | 18 | +| 4336 | `_wpComp()` | 16 | +| 4352 | `_findPathLocal()` | 22 | +| 4374 | `_botCtf()` | 133 | +| 4507 | `_hideCtfHud()` | 6 | +| 4513 | `_updateCtfHud()` | 76 | +| 4589 | `_collide()` | 23 | +| 4612 | `_collideRot()` | 26 | +| 4638 | `_freeSpot()` | 30 | +| 4668 | `_retaAndavel()` | 20 | +| 4688 | `_walkDepth()` | 16 | +| 4704 | `_noteHit()` | 17 | +| 4721 | `_deathFeedback()` | 43 | +| 4764 | `_updateReplayCam()` | 33 | +| 4797 | `_updatePlayer()` | 315 | +| 5112 | `_updatePickups()` | 148 | +| 5260 | `_wpnMode()` | 5 | +| 5265 | `_botWeapon()` | 10 | +| 5275 | `_municaoInfinita()` | 1 | +| 5276 | `_pickupAllowed()` | 7 | +| 5283 | `_grabPickup()` | 35 | +| 5318 | `_assentarNoChao()` | 11 | +| 5329 | `_dropWeapon()` | 18 | +| 5347 | `_sumirDrop()` | 36 | +| 5383 | `_spawnY()` | 3 | +| 5386 | `_pickSpawn()` | 23 | +| 5409 | `_respawnPlayer()` | 25 | +| 5434 | `_losClear()` | 18 | +| 5452 | `_botCall()` | 37 | +| 5489 | `_teamMarkTex()` | 23 | +| 5512 | `_makeTeamMark()` | 14 | +| 5526 | `_updateTeamMark()` | 7 | +| 5533 | `_botEye()` | 1 | +| 5534 | `_enemyOf()` | 8 | +| 5542 | `_duelToken()` | 20 | +| 5562 | `_updateBot()` | 812 | +| 6374 | `_flushTraining()` | 13 | +| 6387 | `_updateBotNN()` | 71 | +| 6458 | `_botShootNN()` | 45 | +| 6503 | `_radarFoot()` | 38 | +| 6541 | `_updateRadar()` | 64 | +| 6605 | `_banner()` | 26 | +| 6631 | `_resultadoDaRodada()` | 4 | +| 6635 | `_showScoreboard()` | 48 | +| 6683 | `_updateWeaponHud()` | 35 | +| 6718 | `_updateHud()` | 80 | +| 6798 | `update()` | 76 | +| 6874 | `dispose()` | 37 |
diff --git a/tools/eval/armas-check.mjs b/tools/eval/armas-check.mjs new file mode 100644 index 00000000..7f10e796 --- /dev/null +++ b/tools/eval/armas-check.mjs @@ -0,0 +1,91 @@ +#!/usr/bin/env node +/* ============================================================================ + armas-check.mjs — A PARTIDA CARREGA AS ARMAS DELA, E NENHUM BOT SEGURA CAIXA + ---------------------------------------------------------------------------- + POR QUE EXISTE (medido em 21/08/2026) + `preloadWeapons()` baixava as 26 armas, BLOQUEANDO, antes de construir o Game — 7,5 MB de + download e ~164 MB de VRAM para uma partida que usa ~9. Era a mesma doença do elenco, que o + #368 já tinha curado nos personagens e ninguém tinha olhado nas armas. + + O QUE ELA NÃO PODE DEIXAR PASSAR + Cortar o preload é fácil; cortar E deixar bot empunhando CAIXA procedural é trocar espera por + feiura. `weaponModel()` cai em `buildRifle()` quando o GLB não está em memória, e isso não + aparece em erro nenhum — some calado dentro da cena. Por isso ARM2 cobra a cena, não o código. + + O QUE MEDE (jogo real, ?auto=, rede de verdade) + ARM1 GLBs de arma no preload BLOQUEANTE <= TETO (12). Janela: do goto até `window.__game` + existir — o Game só nasce depois do `await` do preload. Antes do conserto: 26. + ARM2 todo bot em campo tem arma com malha GLB (zero caixa procedural). + ARM3 o resto das armas CHEGA em ocioso — sem isso, o drop do chão no meio da partida vira + caixa. Sonda até 60 s; o contrato é existência, não velocidade. + + Mutantes (kill-switches reais do jogo): sem-lazy (?armaslazy=0) acende ARM3. + Uso: node tools/eval/armas-check.mjs [--mutante=sem-lazy] [--porta=8137] + ============================================================================ */ +import { execSync, spawn } from 'node:child_process'; +import { pathToFileURL } from 'node:url'; + +const arg = (n) => (process.argv.find((a) => a.startsWith(`--${n}=`)) || '').split('=')[1] || ''; +const MUT = arg('mutante'); +if (MUT && MUT !== 'sem-lazy') throw new Error(`mutante desconhecido: ${MUT}`); +const PORTA = arg('porta') || '8137'; +const BASE = `http://127.0.0.1:${PORTA}`; +const TETO = 12; + +const gRoot = execSync('npm root -g').toString().trim(); +const _pw = await import(pathToFileURL(`${gRoot}/playwright/index.js`).href); +const chromium = _pw.chromium || _pw.default?.chromium; + +const srv = spawn('node', ['tools/eval/serve.mjs', PORTA], { stdio: 'ignore' }); +process.on('exit', () => srv.kill()); +for (let i = 0; i < 60; i++) { + try { if ((await fetch(BASE)).ok) break; } catch { /* subindo */ } + await new Promise((r) => setTimeout(r, 500)); +} + +const browser = await chromium.launch({ args: ['--use-angle=swiftshader', '--enable-unsafe-swiftshader', '--mute-audio'] }); +const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); +const ARMA_GLB = /\/models\/weapons\/([a-z0-9-]+)\.glb/; +const pedidas = []; +page.on('request', (r) => { const m = ARMA_GLB.exec(r.url()); if (m) pedidas.push({ id: m[1], t: Date.now() }); }); + +const falhas = []; +try { + await page.goto(`${BASE}/?debug=1&auto=P,mst&map=piscina_treta${MUT === 'sem-lazy' ? '&armaslazy=0' : ''}`, + { waitUntil: 'load', timeout: 180000 }); + await page.waitForFunction(() => window.__game, null, { timeout: 180000 }); + const bloqueante = new Set(pedidas.map((p) => p.id)).size; + + await page.waitForFunction(() => window.__game?.state === 'live', null, { timeout: 180000 }); + /* glbchars nomeia a peça: 'arma-glb' quando o GLB entrou, 'arma-caixa' no fallback. */ + const cena = await page.evaluate(() => { + const g = window.__game; + const semGlb = g.bots.filter((b) => b.mesh?.group?.getObjectByName('arma-caixa')).map((b) => b.weapon); + return { bots: g.bots.length, semGlb: [...new Set(semGlb)] }; + }); + + let esperou = 0, total = new Set(pedidas.map((p) => p.id)).size; + while (total < 20 && esperou < 60000) { + await page.waitForTimeout(2000); esperou += 2000; + total = new Set(pedidas.map((p) => p.id)).size; + } + + console.log(` ARM1 armas no bloqueante (<= ${TETO}): ${bloqueante} ${bloqueante <= TETO ? 'OK' : 'FALHOU'}`); + console.log(` ARM2 bots com arma de caixa (== 0): ${cena.semGlb.length} de ${cena.bots} bots${cena.semGlb.length ? ` — ${cena.semGlb.join(', ')}` : ''}`); + console.log(` ARM3 armas carregadas ao todo: ${total} em ${(esperou / 1000).toFixed(0)}s`); + if (bloqueante > TETO) falhas.push(`ARM1 preload bloqueante com ${bloqueante} armas (teto ${TETO})`); + if (cena.semGlb.length) falhas.push(`ARM2 ${cena.semGlb.length} bot(s) empunhando caixa: ${cena.semGlb.join(', ')}`); + if (total <= bloqueante) falhas.push(`ARM3 carga tardia não chegou: parou em ${total} armas`); +} catch (e) { + falhas.push(`ARM não deu para medir: ${String(e).split('\n')[0]}`); +} + +await browser.close(); +srv.kill(); +for (const f of falhas) console.log(` \x1b[31m✗\x1b[0m ${f}`); +if (!falhas.length) console.log(' \x1b[32m✓\x1b[0m ARM partida carrega as armas dela, nenhum bot com caixa e o resto chega em ocioso'); +if (MUT && !falhas.length) { + console.log(` \x1b[31m✗\x1b[0m MUTAÇÃO '${MUT}' não acendeu nenhuma cláusula — portão cego (lei 3)`); + falhas.push('mutacao-cega'); // prova que não morde é vermelho, não aviso +} +process.exit(falhas.length ? 1 : 0); diff --git a/tools/eval/autofix-check.mjs b/tools/eval/autofix-check.mjs new file mode 100644 index 00000000..ec57c443 --- /dev/null +++ b/tools/eval/autofix-check.mjs @@ -0,0 +1,141 @@ +#!/usr/bin/env node +/* ============================================================================ + autofix-check.mjs — O BOT CONSERTA, MAS SÓ O QUE É DERIVADO + ---------------------------------------------------------------------------- + O autofix é a primeira máquina deste repositório com permissão de ESCREVER num + PR. Isso só é aceitável enquanto três coisas valerem, e esta régua é quem as + segura: + + AF1 · toda escrita passa pela lista de permissão. Um `git commit`/`git push` no + autofix.yml que não seja precedido pela trava é a régua inteira contornada. + AF2 · a lista nunca cobre caminho de código. `public/`, `src/`, `scripts/`, + `.github/` e os manifestos ficam de fora — inclusive `.github/`, porque um + bot que edita o workflow que o governa amplia a própria permissão. + AF3 · o bot não mergeia. Deixar o PR pronto e fechá-lo são coisas diferentes, e + a segunda é humana (decisão do dono, 21/08). + AF4 · e isso vale para o repositório INTEIRO, não só para o autofix: nenhum + workflow pode chamar `gh pr merge`. O `csbrasil-bot-automerge` chamava, e + era a única coisa que um bot daqui fazia sozinho — justamente a que não + devia. Ele agora aplica `pronto-pra-merge` e o botão continua humano. + AF6 · o autofix acorda quando a MAIN anda, não só quando o PR se mexe. Foram 9 + releases em 20 horas e cada um reabre conflito em todo PR aberto; sem o + gatilho de push o bot só conserta quem empurra commit — ou seja, nunca + quem está parado esperando revisão, que é justamente quem precisa. + AF5 · o resolvedor de conflito passa pela MESMA trava e ABORTA quando sobra + conflito fora dela. Resolver conflito de arquivo gerado é mecânico (todo + `chore(release)` reabre um em cada PR aberto); resolver conflito de código + é julgamento. Sem o `git merge --abort` no caminho de exceção, o bot + resolveria código escolhendo um lado no escuro. + + Uso: node tools/eval/autofix-check.mjs [--mutante=] + ============================================================================ */ +import { readFileSync, readdirSync } from 'node:fs'; + +const MUT = (process.argv.find((a) => a.startsWith('--mutante=')) || '').split('=')[1] || ''; +const MUTANTES = { + 'autofix-sem-trava': 'AF1', + 'lista-abre-codigo': 'AF2', + 'lista-abre-workflow': 'AF2', + 'autofix-mergeia': 'AF3', + 'automerge-volta': 'AF4', + 'resolve-conflito-de-codigo': 'AF5', + 'sem-varredura-pos-release': 'AF6', +}; +if (MUT && !MUTANTES[MUT]) { console.error(`mutante desconhecido: ${MUT}`); process.exit(2); } + +const ler = (p) => { try { return readFileSync(p, 'utf8'); } catch { return ''; } }; +const falhas = []; + +let wf = ler('.github/workflows/autofix.yml'); +let lista = ler('scripts/ci/autofix_allowlist.py'); + +if (MUT === 'autofix-sem-trava') wf = wf.replace(/autofix_allowlist\.py/g, 'cat'); +if (MUT === 'autofix-mergeia') wf = wf.replace('git push origin "HEAD:$HEAD_REF"', 'git push origin "HEAD:$HEAD_REF"\n gh pr merge "$PR_NUMBER" --squash --auto'); +if (MUT === 'lista-abre-codigo') lista = lista.replace(' "docs/",', ' "docs/",\n "public/",'); +if (MUT === 'lista-abre-workflow') lista = lista.replace(/^\s*"\.github\/",$/m, ''); + +if (!wf) { console.log(' \x1b[31m✗\x1b[0m AF0 .github/workflows/autofix.yml não existe'); process.exit(1); } + +/* ---- AF1: escrita só depois da trava ---- */ +const usaTrava = /autofix_allowlist\.py/.test(wf); +const escreve = /git (commit|push)/.test(wf); +const posTrava = wf.indexOf('autofix_allowlist.py'); +const posCommit = wf.search(/git commit/); +if (escreve && (!usaTrava || posCommit < posTrava)) { + falhas.push('AF1 autofix.yml escreve no PR sem passar pela lista de permissão antes'); +} +/* O commit tem de usar SÓ o que a trava liberou, não um `git add -A`. */ +if (/git add\s+(-A|--all|\.)(\s|$)/.test(wf)) { + falhas.push('AF1 autofix.yml usa `git add` abrangente — o commit deve sair da saída da trava'); +} + +/* ---- AF2: a lista não cobre código ---- */ +if (!lista) falhas.push('AF2 scripts/ci/autofix_allowlist.py não existe'); +else { + const bloco = (lista.match(/PERMITIDOS = \(([\s\S]*?)\)/) || [])[1] || ''; + const proibidos = (lista.match(/PROIBIDOS = \(([\s\S]*?)\)/) || [])[1] || ''; + for (const perigoso of ['public/', 'src/', 'supabase/', 'scripts/', '.github/']) { + if (new RegExp(`"${perigoso.replace('.', '\\.')}`).test(bloco)) { + falhas.push(`AF2 a lista de permissão cobre \`${perigoso}\` — o bot passa a poder reescrever código`); + } + } + for (const obrigatorio of ['.github/', 'scripts/', 'package.json']) { + if (!new RegExp(`"${obrigatorio.replace('.', '\\.')}`).test(proibidos)) { + falhas.push(`AF2 \`${obrigatorio}\` não está na lista de proibidos`); + } + } +} + +/* ---- AF3: o bot não mergeia ---- */ +if (/gh pr merge/.test(wf)) { + falhas.push('AF3 autofix.yml mergeia PR — o autofix deixa pronto, quem fecha é gente'); +} + +/* ---- AF5: o resolvedor de conflito aborta quando o conflito é de gente ---- */ +if (MUT === 'resolve-conflito-de-codigo') { + wf = wf.replace(/\s*git merge --abort\n/, '\n'); +} +const resolveConflito = /--caminhos/.test(wf) && /git checkout --theirs/.test(wf); +if (resolveConflito) { + const trecho = wf.slice(wf.indexOf('diff-filter=U'), wf.indexOf('git checkout --theirs')); + if (!/autofix_allowlist\.py --caminhos/.test(trecho)) { + falhas.push('AF5 o resolvedor escolhe lado do conflito sem consultar a lista de permissão'); + } + if (!/git merge --abort/.test(trecho)) { + falhas.push('AF5 o resolvedor não aborta o merge quando sobra conflito fora da lista — passaria a decidir código no escuro'); + } + if (!/--caminhos/.test(ler('scripts/ci/autofix_allowlist.py'))) { + falhas.push('AF5 autofix_allowlist.py não entende `--caminhos`, que é como o resolvedor o consulta'); + } +} + +/* ---- AF6: a varredura pós-release existe e enxerga quem ficou para trás ---- */ +if (MUT === 'sem-varredura-pos-release') wf = wf.replace(/\n push:\n branches: \[main\]/, ''); +const temPush = /^\s{2}push:\n\s{4}branches: \[main\]/m.test(wf); +const temVarredura = /varredura:\n\s+if: github\.event_name == 'push'/.test(wf); +const filtraAtrasado = /mergeable != "MERGEABLE"/.test(wf) && /gh workflow run autofix\.yml/.test(wf); +if (!temPush) falhas.push('AF6 autofix.yml não acorda quando a main anda — PR parado esperando revisão nunca é consertado'); +else if (!temVarredura) falhas.push('AF6 o gatilho de push existe mas não há job de varredura'); +else if (!filtraAtrasado) falhas.push('AF6 a varredura não seleciona os PRs desatualizados nem dispara o autofix de cada um'); + +/* ---- AF4: e nenhum outro workflow mergeia tampouco ---- */ +const WORKFLOWS = readdirSync('.github/workflows').filter((f) => /\.ya?ml$/.test(f)); +for (const nome of WORKFLOWS) { + let texto = ler(`.github/workflows/${nome}`); + if (MUT === 'automerge-volta' && nome === 'csbrasil-bot-automerge.yml') { + texto += "\n subprocess.run(['gh', 'pr', 'merge', pr, '--squash', '--auto'])\n"; + } + /* Só a CHAMADA conta: o comentário que explica por que ela saiu tem de poder citá-la. */ + const linhas = texto.split('\n').filter((l) => !/^\s*#/.test(l)); + if (linhas.some((l) => /gh['"]?,\s*['"]pr['"],\s*['"]merge['"]|gh pr merge/.test(l))) { + falhas.push(`AF4 ${nome} mergeia PR sozinho — deixar pronto e fechar são coisas diferentes`); + } +} + +for (const f of falhas) console.log(` \x1b[31m✗\x1b[0m ${f}`); +if (!falhas.length) console.log(' \x1b[32m✓\x1b[0m AF autofix escreve só o que a trava libera, a lista não cobre código e o bot não mergeia'); +if (MUT && !falhas.length) { + console.log(` \x1b[31m✗\x1b[0m MUTAÇÃO '${MUT}' não acendeu nenhuma cláusula — portão cego`); + falhas.push('mutacao-cega'); // prova que não morde é vermelho, não aviso (MC1) +} +process.exit(falhas.length ? 1 : 0); diff --git a/tools/eval/deploy-gate-check.mjs b/tools/eval/deploy-gate-check.mjs new file mode 100644 index 00000000..6b5e5716 --- /dev/null +++ b/tools/eval/deploy-gate-check.mjs @@ -0,0 +1,75 @@ +#!/usr/bin/env node +/* ============================================================================ + deploy-gate-check.mjs — QUEM BLOQUEIA O PR É O CI, NÃO A VERCEL + ---------------------------------------------------------------------------- + MEDIDO EM 21/08/2026: Vercel vermelha em 13 dos 14 PRs vindos de fork, sempre + com `Authorization required to deploy.` - a proteção de fork da própria Vercel, + que não se resolve por commit nenhum. E em 4 PRs de casa o vermelho era build + real, uma vez por `fetch-decals.sh` não ter baixado o acervo (196 de 197 + decalques faltando) num soluço de rede. + + Para poder tirar a Vercel do caminho crítico do PR, duas coisas precisam valer: + + DG1 · o CI constrói o site no próprio PR. Sem isso, tirar a Vercel do bloqueio + deixaria o PR sem NENHUMA prova de que o site compila. + DG2 · nenhum download do caminho do build corre sem retry. `set -e` mais um + `curl` sem `--retry` transforma soluço de rede em PR vermelho, e o autor + não tem o que consertar - é o mesmo dano do portão que não pode medir. + DG3 · o preview de fork roda sem pedir aprovação a ninguém, porque quem compila + não tem segredo e quem tem segredo não compila. Os dois arquivos existem e o + contrato deles é guardado em detalhe pelo `scripts/ci/workflow_security_check.py` + (PRV1/PRV2/PRV3); aqui só se confere que o par não sumiu. + + Uso: node tools/eval/deploy-gate-check.mjs [--mutante=] + ============================================================================ */ +import { readFileSync, readdirSync } from 'node:fs'; + +const MUT = (process.argv.find((a) => a.startsWith('--mutante=')) || '').split('=')[1] || ''; +const MUTANTES = { 'ci-sem-build': 'DG1', 'download-sem-retry': 'DG2', 'preview-sem-par': 'DG3' }; +if (MUT && !MUTANTES[MUT]) { console.error(`mutante desconhecido: ${MUT}`); process.exit(2); } + +const ler = (p) => { try { return readFileSync(p, 'utf8'); } catch { return ''; } }; +const falhas = []; + +/* ---- DG1: o CI constrói no PR ---- */ +let ci = ler('.github/workflows/ci.yml'); +if (MUT === 'ci-sem-build') ci = ci.replace('run: npm run build', 'run: echo pulei o build'); +const on = (ci.match(/^on:\n([\s\S]*?)(?=^\S)/m) || [])[1] || ''; +if (!/^\s{2}pull_request:/m.test(on) || !/run:\s*npm run build/.test(ci)) { + falhas.push('DG1 ci.yml não constrói o site em pull_request — sem isso a Vercel não pode sair do bloqueio'); +} + +/* ---- DG2: download do build com retry ---- */ +const FETCHERS = readdirSync('scripts').filter((f) => /^fetch-.*\.sh$/.test(f)); +for (const f of FETCHERS) { + let sh = ler(`scripts/${f}`); + if (MUT === 'download-sem-retry' && f === 'fetch-decals.sh') { + sh = sh.replace(/curl --retry \d+[^"]*-fsSL/, 'curl -fsSL'); + } + for (const linha of sh.split('\n')) { + if (!/^\s*curl\s/.test(linha)) continue; + if (!/--retry\b/.test(linha)) { + falhas.push(`DG2 scripts/${f}: curl sem --retry no caminho do build — soluço de rede vira PR vermelho`); + } + } +} +if (!FETCHERS.length) falhas.push('DG2 nenhum scripts/fetch-*.sh encontrado — a régua perdeu o alvo'); + +/* ---- DG3: a etiqueta do preview nasce com quem a usa ---- */ +let preview = ler('.github/workflows/preview-bot.yml'); +if (MUT === 'preview-sem-par') preview = preview.replace(/ensure_labels\.py preview-autorizado/, 'ensure_labels.py'); +const temBuild = ler('.github/workflows/preview-build.yml'); +const temDeploy = ler('.github/workflows/preview-deploy.yml'); +if (MUT === 'preview-sem-par') { /* nome antigo do mutante: agora derruba o par */ } +const par = (MUT === 'preview-sem-par') ? '' : (temBuild && temDeploy); +if (!par) { + falhas.push('DG3 falta preview-build.yml ou preview-deploy.yml — sem o par, preview de fork volta a depender de aprovação humana'); +} + +for (const f of falhas) console.log(` \x1b[31m✗\x1b[0m ${f}`); +if (!falhas.length) console.log(` \x1b[32m✓\x1b[0m DG o PR é bloqueado pelo build do CI; download tem retry (${FETCHERS.length} fetchers); preview de fork sem aprovação`); +if (MUT && !falhas.length) { + console.log(` \x1b[31m✗\x1b[0m MUTAÇÃO '${MUT}' não acendeu nenhuma cláusula — portão cego`); + falhas.push('mutacao-cega'); // prova que não morde é vermelho, não aviso (MC1) +} +process.exit(falhas.length ? 1 : 0); diff --git a/tools/eval/docs-autoria-check.mjs b/tools/eval/docs-autoria-check.mjs index f5d27c42..e7bac2e1 100644 --- a/tools/eval/docs-autoria-check.mjs +++ b/tools/eval/docs-autoria-check.mjs @@ -46,12 +46,18 @@ if (sujo) { process.exit(1); } -// Linha de base: medir mutação sobre árvore já desatualizada dá veredito sem valor. -if (spawnSync('node', [GERADOR, '--check', '--autoria'], { encoding: 'utf8' }).status !== 0) { +/* Linha de base SEM `--autoria`: com ele, qualquer branch de PR que traga autor novo no + histórico reprovava aqui — o contrato do #398 é que autoria só é derivável na main, e a régua + estava cobrando o contrário do que guarda (bateu em #375, #372, #399). */ +if (spawnSync('node', [GERADOR, '--check'], { encoding: 'utf8' }).status !== 0) { console.log(' \x1b[31m✗\x1b[0m DOCSAUT não dá para medir: os blocos gerados já estão desatualizados (rode: npm run docs)'); process.exit(1); } +/* A DOCSAUT2 pergunta se `--autoria` MORDE a autoria torta. Numa branch onde ela já está torta + por si, a pergunta não tem resposta: pular dizendo isso é honesto, cobrar não. */ +const autoriaMedivel = spawnSync('node', [GERADOR, '--check', '--autoria'], { encoding: 'utf8' }).status === 0; + const original = readFileSync(ALVO, 'utf8'); const geradorOriginal = readFileSync(GERADOR, 'utf8'); const rodar = (args) => spawnSync('node', [GERADOR, '--check', ...args], { encoding: 'utf8' }).status === 0; @@ -75,7 +81,8 @@ try { if (comAutoriaTorta === original) throw new Error('não achei a frase de autoria para mutar'); writeFileSync(ALVO, comAutoriaTorta); if (!rodar([])) falhas.push('DOCSAUT1 autoria torta REPROVOU o PR — cobra de quem não pode consertar (só é derivável após o squash)'); - if (rodar(['--autoria'])) falhas.push('DOCSAUT2 autoria torta PASSOU com --autoria — o portão da main está cego para o único lugar onde o número é derivável'); + if (!autoriaMedivel) console.log(' \x1b[33m⚠\x1b[0m DOCSAUT2 pulada: a autoria já está torta nesta branch (autor novo no histórico), então a mutação não é medível aqui'); + if (autoriaMedivel && rodar(['--autoria'])) falhas.push('DOCSAUT2 autoria torta PASSOU com --autoria — o portão da main está cego para o único lugar onde o número é derivável'); writeFileSync(ALVO, original); // ---- mutação de OUTRO bloco: a tolerância não pode ter vazado para o resto @@ -93,5 +100,8 @@ try { for (const f of falhas) console.log(` \x1b[31m✗\x1b[0m ${f}`); if (!falhas.length) console.log(' \x1b[32m✓\x1b[0m DOCSAUT autoria só é cobrada onde é derivável (main); os outros blocos mordem sempre'); -if (MUT && !falhas.length) console.log(` \x1b[31m✗\x1b[0m MUTAÇÃO '${MUT}' não acendeu nenhuma cláusula — portão cego (lei 3)`); +if (MUT && !falhas.length) { + console.log(` \x1b[31m✗\x1b[0m MUTAÇÃO '${MUT}' não acendeu nenhuma cláusula — portão cego (lei 3)`); + falhas.push('mutacao-cega'); // prova que não morde é vermelho, não aviso +} process.exit(falhas.length ? 1 : 0); diff --git a/tools/eval/entrada-check.mjs b/tools/eval/entrada-check.mjs index 1ba52406..15972a88 100644 --- a/tools/eval/entrada-check.mjs +++ b/tools/eval/entrada-check.mjs @@ -95,5 +95,8 @@ await browser.close(); srv.kill(); for (const f of falhas) console.log(` \x1b[31m✗\x1b[0m ${f}`); if (!falhas.length) console.log(' \x1b[32m✓\x1b[0m ENTRADA1 o gesto que entra tira a splash e NÃO aperta nada no menu'); -if (MUT && !falhas.length) console.log(` \x1b[31m✗\x1b[0m MUTAÇÃO '${MUT}' não acendeu nenhuma cláusula — portão cego (lei 3)`); +if (MUT && !falhas.length) { + console.log(` \x1b[31m✗\x1b[0m MUTAÇÃO '${MUT}' não acendeu nenhuma cláusula — portão cego (lei 3)`); + falhas.push('mutacao-cega'); // prova que não morde é vermelho, não aviso +} process.exit(falhas.length ? 1 : 0); diff --git a/tools/eval/fixture-check.mjs b/tools/eval/fixture-check.mjs index 987f43be..a10fd1b4 100644 --- a/tools/eval/fixture-check.mjs +++ b/tools/eval/fixture-check.mjs @@ -35,7 +35,6 @@ const base = arg('base') || 'origin/main'; /* Dívida herdada: script de decisão que nasceu sem fixture. A lista só encolhe. Quem tocar num destes escreve o `--selftest` no mesmo PR. */ const SEM_FIXTURE = new Set([ - 'scripts/ci/ensure_labels.py', 'scripts/ci/issue_triage.py', 'scripts/ci/pr_classify.py', 'scripts/ci/pr_route.py', diff --git a/tools/eval/mutacao-cega-check.mjs b/tools/eval/mutacao-cega-check.mjs new file mode 100644 index 00000000..ef6d9895 --- /dev/null +++ b/tools/eval/mutacao-cega-check.mjs @@ -0,0 +1,60 @@ +#!/usr/bin/env node +/* ============================================================================ + mutacao-cega-check.mjs — A PROVA DE QUE A RÉGUA MORDE NÃO PODE SAIR VERDE + ---------------------------------------------------------------------------- + A casa tem uma lei: régua que não morde não existe, e quem demonstra a mordida + é o modo `--mutante=`. O problema achado em 21/08: cinco réguas imprimiam + + ✗ MUTAÇÃO 'x' não acendeu nenhuma cláusula — portão cego (lei 3) + + e logo abaixo chamavam `process.exit(falhas.length ? 1 : 0)`. Com a lista de + falhas vazia — que é exatamente o caso do portão cego — o processo saía com + ZERO. Quem roda a mutação num laço e confere o código de saída (o jeito óbvio de + automatizar isso) via VERDE justamente na hora em que a régua se mostrou cega. + + A prova da mordida não pode ter o mesmo defeito que ela existe para caçar. + + MC1 · toda régua que anuncia mutação cega tem de REPROVAR nesse caso. + + Uso: node tools/eval/mutacao-cega-check.mjs [--mutante=exit-cego] + ============================================================================ */ +import { readFileSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; + +const DIR = 'tools/eval'; +const MUT = (process.argv.find((a) => a.startsWith('--mutante=')) || '').split('=')[1] || ''; +if (MUT && MUT !== 'exit-cego') { console.error(`mutante desconhecido: ${MUT}`); process.exit(2); } + +const falhas = []; +let auditadas = 0; + +for (const nome of readdirSync(DIR).filter((f) => f.endsWith('.mjs'))) { + let texto = readFileSync(join(DIR, nome), 'utf8'); + if (MUT === 'exit-cego' && nome === 'docs-autoria-check.mjs') { + texto = texto.replace(/\n\s*falhas\.push\('mutacao-cega'\);[^\n]*/, ''); + } + + /* Só interessa quem TEM o modo mutante e anuncia o caso cego: régua sem mutação + não está mentindo sobre uma prova que não faz. */ + if (!/não acendeu nenhuma cláusula/.test(texto)) continue; + auditadas++; + + /* Duas formas de fazer certo, e a casa já usa as duas: empurrar o caso na lista de + falhas (telemetry-check) ou levar uma flag até o `process.exit`. O que não vale é + imprimir o aviso e sair pelo `falhas.length`, que ali é zero por definição. */ + const depois = texto.slice(texto.indexOf('não acendeu nenhuma cláusula')); + const entraNasFalhas = /falhas\.push/.test(depois.slice(0, 300)); + const saida = texto.match(/process\.exit(?:Code)?\s*[(=]\s*([^;)]*)\)?/g) || []; + const flagNoExit = saida.some((e) => /mutacaoCega|cega/i.test(e)); + if (!entraNasFalhas && !flagNoExit) { + falhas.push(`MC1 ${nome}: anuncia "portão cego" e sai por \`${saida[saida.length - 1] || 'nada'}\` — a lista de falhas está vazia nesse caso, então o processo sai VERDE`); + } +} + +if (!auditadas) falhas.push('MC1 nenhuma régua com modo mutante encontrada — a varredura perdeu o alvo'); + +for (const f of falhas) console.log(` \x1b[31m✗\x1b[0m ${f}`); +if (!falhas.length) console.log(` \x1b[32m✓\x1b[0m MC1 as ${auditadas} réguas com modo mutante reprovam quando a mutação não morde`); +const mutacaoCega = MUT && !falhas.length; +if (mutacaoCega) console.log(` \x1b[31m✗\x1b[0m MUTAÇÃO '${MUT}' não acendeu nenhuma cláusula — portão cego (lei 3)`); +process.exit(falhas.length || mutacaoCega ? 1 : 0); diff --git a/tools/eval/portao-inteiro-check.mjs b/tools/eval/portao-inteiro-check.mjs new file mode 100644 index 00000000..b0b361e8 --- /dev/null +++ b/tools/eval/portao-inteiro-check.mjs @@ -0,0 +1,68 @@ +#!/usr/bin/env node +/* ============================================================================ + portao-inteiro-check.mjs — PORTÃO NÃO PODE CITAR PASSO QUE NÃO EXISTE + ---------------------------------------------------------------------------- + `check:fast` e `check:deploy` são listas de nomes de script. Se um nome citado + ali não está definido em `scripts`, o npm morre com "missing script" e o portão + inteiro para no meio - os passos DEPOIS do nome quebrado nunca rodam. + + DOIS CASOS REAIS, os dois em 22/08/2026: + · o PR #372 citava `eval:gelo` no `check:fast` e nunca definiu o script. A régua + `treta-gelo-check.mjs` existia e simplesmente não estava ligada em lugar nenhum; + · resolvendo o conflito de `package.json` do #408, eu fiquei com a linha do + `check:` de um lado e perdi as DEFINIÇÕES do outro. Três réguas viraram nome + órfão de uma vez - e conflito de `package.json` é justamente o que mais + acontece, porque todo PR que acrescenta régua mexe na mesma linha. + + PI1 · todo nome citado em `check:fast`/`check:deploy` existe em `scripts`. + PI2 · todo script `eval:*` aponta para arquivo que existe em disco. + + Uso: node tools/eval/portao-inteiro-check.mjs [--mutante=] + ============================================================================ */ +import { readFileSync, existsSync } from 'node:fs'; + +const MUT = (process.argv.find((a) => a.startsWith('--mutante=')) || '').split('=')[1] || ''; +const MUTANTES = { 'passo-orfao': 'PI1', 'regua-fantasma': 'PI2' }; +if (MUT && !MUTANTES[MUT]) { console.error(`mutante desconhecido: ${MUT}`); process.exit(2); } + +const pkg = JSON.parse(readFileSync('package.json', 'utf8')); +const scripts = { ...pkg.scripts }; +if (MUT === 'passo-orfao') scripts['check:fast'] += ' eval:nao-existe'; +if (MUT === 'regua-fantasma') scripts['eval:fantasma'] = 'node tools/eval/nao-existe-check.mjs'; + +const falhas = []; +const PORTOES = ['check:fast', 'check:deploy']; + +/* ---- PI1: nome citado tem de estar definido ---- */ +for (const portao of PORTOES) { + const linha = scripts[portao]; + if (!linha) { falhas.push(`PI1 o portão \`${portao}\` não existe em scripts`); continue; } + /* A linha é `node tools/eval/runner.mjs a b c`: passo é token com `:` que não é + caminho. `syntax` é o único passo sem `:` e está definido como script. */ + const passos = linha.split(/\s+/).slice(2).filter((t) => t && !t.includes('/')); + for (const passo of passos) { + if (!scripts[passo]) { + falhas.push(`PI1 ${portao} cita \`${passo}\`, que não está definido em scripts — o npm morre em "missing script" e os passos seguintes NÃO rodam`); + } + } +} + +/* ---- PI2: régua citada tem de existir em disco ---- */ +for (const [nome, cmd] of Object.entries(scripts)) { + if (nome.startsWith('//')) continue; // chave de documentação, não é script + const m = String(cmd).match(/node\s+(tools\/[^\s]+\.mjs)/); + if (m && !existsSync(m[1])) { + falhas.push(`PI2 \`${nome}\` aponta para ${m[1]}, que não existe em disco`); + } +} + +for (const f of falhas) console.log(` \x1b[31m✗\x1b[0m ${f}`); +if (!falhas.length) { + const total = PORTOES.reduce((n, p) => n + (scripts[p] || '').split(/\s+/).slice(2).filter((t) => t && !t.includes('/')).length, 0); + console.log(` \x1b[32m✓\x1b[0m PI todo passo dos dois portões está definido e aponta para arquivo que existe (${total} citações)`); +} +if (MUT && !falhas.length) { + console.log(` \x1b[31m✗\x1b[0m MUTAÇÃO '${MUT}' não acendeu nenhuma cláusula — portão cego (lei 3)`); + falhas.push('mutacao-cega'); // prova que não morde é vermelho, não aviso (MC1) +} +process.exit(falhas.length ? 1 : 0); diff --git a/tools/eval/preload-check.mjs b/tools/eval/preload-check.mjs index 0d530be6..e04b741f 100644 --- a/tools/eval/preload-check.mjs +++ b/tools/eval/preload-check.mjs @@ -54,7 +54,19 @@ const _derivaDoRoster = (expr) => { } return false; }; -const _chamadas = [...main.matchAll(/preloadCharacterAssets\(([^)]*(?:\([^)]*\))?[^)]*)\)/g)].map((m) => m[1]); +/* Só o PRIMEIRO argumento: a chamada ganhou um 2º (a lista de armas da partida), e seguir a + cadeia do texto inteiro dava a lista errada. Vírgula de topo, sem entrar em {...} nem (...). */ +const _primeiro = (txt) => { + let prof = 0; + for (let i = 0; i < txt.length; i++) { + const c = txt[i]; + if ('([{'.includes(c)) prof++; + else if (')]}'.includes(c)) prof--; + else if (c === ',' && prof === 0) return txt.slice(0, i); + } + return txt; +}; +const _chamadas = [...main.matchAll(/preloadCharacterAssets\(([\s\S]*?)\)[,;\s]/g)].map((m) => _primeiro(m[1])); if (!_chamadas.length) falhas.push('PL1 nenhum preload de personagem no main.js'); if (_chamadas.some((a) => /GLB_CHARS/.test(a))) falhas.push('PL1 preload voltou a subir o elenco inteiro (44 GLBs no boot da partida)'); @@ -75,6 +87,8 @@ if (!/hasModel\(c\.id\)/.test(game)) for (const f of falhas) console.log(` \x1b[31m✗\x1b[0m ${f}`); if (!falhas.length) console.log(' \x1b[32m✓\x1b[0m PL1 preload da partida = roster sorteado (~8 GLBs), Game honra, M não materializa fora do carregado'); -if (MUT && !falhas.length) +if (MUT && !falhas.length) { console.log(` \x1b[31m✗\x1b[0m MUTAÇÃO '${MUT}' não acendeu nenhuma cláusula — portão cego (lei 3)`); + falhas.push('mutacao-cega'); // prova que não morde é vermelho, não aviso +} process.exit(falhas.length ? 1 : 0); diff --git a/tools/eval/redesign-check.mjs b/tools/eval/redesign-check.mjs index a0651606..6442d475 100644 --- a/tools/eval/redesign-check.mjs +++ b/tools/eval/redesign-check.mjs @@ -582,7 +582,7 @@ const loadingCharacterIds = [...loading3d.matchAll(/[EBUCF]: '([^']+)'/g)].map(( const loadingCanvas3d = /id="load-character-3d"[^>]*><\/canvas>/.test(astro) && !/id="load-char" class="loading-runner"/.test(astro) && /export class LoadingCharacterStage/.test(loading3d) - && /preloadCharacterAssets\(\[id\]\)/.test(loading3d) + && /preloadCharacterAssets\(\[id\](,\s*\{[\s\S]*?\})?\)/.test(loading3d) && /buildCharacterModel\(def, \{ weaponId: charWeapon\(id\), preview: true \}\)/.test(loading3d) && /criaRenderer\(\{ canvas, alpha: true/.test(loading3d) && /this\.renderer\.render\(this\.scene, this\.camera\)/.test(loading3d); diff --git a/tools/eval/replaycam-check.mjs b/tools/eval/replaycam-check.mjs index 354e5eab..3229de83 100644 --- a/tools/eval/replaycam-check.mjs +++ b/tools/eval/replaycam-check.mjs @@ -130,5 +130,8 @@ function alvo(g) { for (const f of falhas) console.log(` \x1b[31m✗\x1b[0m ${f}`); if (!falhas.length) console.log(` \x1b[32m✓\x1b[0m RC replay cam: só no headshot do jogador, termina devolvendo o FOV, ?replaycam=0 desliga — ${medido}`); -if (MUT && !falhas.length) console.log(` \x1b[31m✗\x1b[0m MUTAÇÃO '${MUT}' não acendeu nenhuma cláusula — portão cego (lei 3)`); +if (MUT && !falhas.length) { + console.log(` \x1b[31m✗\x1b[0m MUTAÇÃO '${MUT}' não acendeu nenhuma cláusula — portão cego (lei 3)`); + falhas.push('mutacao-cega'); // prova que não morde é vermelho, não aviso +} process.exit(falhas.length ? 1 : 0); diff --git a/tools/eval/workflow-secret-check.mjs b/tools/eval/workflow-secret-check.mjs new file mode 100644 index 00000000..37254ec0 --- /dev/null +++ b/tools/eval/workflow-secret-check.mjs @@ -0,0 +1,74 @@ +#!/usr/bin/env node +/* ============================================================================ + workflow-secret-check.mjs — PORTÃO QUE NÃO PODE MEDIR NÃO VOTA + ---------------------------------------------------------------------------- + MEDIDO EM 21/08/2026, nos últimos 29 PRs: `classify` vermelho em 11 dos 14 PRs + vindos de FORK e em 0 dos 15 de casa. A causa não era o código de ninguém. + + A regra do GitHub: um workflow disparado por `pull_request`, `pull_request_review` + ou `pull_request_review_comment` a partir de um FORK roda SEM os secrets do + repositório. Só `pull_request_target`, `workflow_dispatch`, `schedule` e + `workflow_run` correm no contexto base e enxergam secret. + + Um job que (a) escuta um desses gatilhos cegos e (b) sai com erro quando o secret + está vazio, reprova todo PR externo por um motivo que o autor não tem como + consertar - e ainda ensina o time a ignorar vermelho, que é o dano caro. + + O QUE ESTA RÉGUA EXIGE: se o job pode ser disparado por gatilho cego, ele precisa + de um `if:` que o exclua desse caso. O guard de secret continua podendo existir e + continuar falhando alto onde o secret DEVERIA estar lá. + + Uso: node tools/eval/workflow-secret-check.mjs [--mutante=] + ============================================================================ */ +import { readFileSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; + +const DIR = '.github/workflows'; +const MUT = (process.argv.find((a) => a.startsWith('--mutante=')) || '').split('=')[1] || ''; +const MUTANTES = { 'guard-sem-if': 'WSEC1', 'gatilho-cego-novo': 'WSEC1' }; +if (MUT && !MUTANTES[MUT]) { console.error(`mutante desconhecido: ${MUT}`); process.exit(2); } + +/* Gatilhos que, num PR de fork, chegam sem secret. `pull_request` entra na lista + porque é o caso clássico; os outros dois foram os que sangraram aqui. */ +const CEGOS = ['pull_request', 'pull_request_review', 'pull_request_review_comment']; + +const arquivos = readdirSync(DIR).filter((f) => f.endsWith('.yml') || f.endsWith('.yaml')); +const falhas = []; + +for (const nome of arquivos) { + let texto = readFileSync(join(DIR, nome), 'utf8'); + if (MUT === 'guard-sem-if' && nome === 'csbrasil-bot-pr-classify.yml') { + texto = texto.replace(/ if: >-\n( github\.event_name[^\n]*\n)+/, ''); + } + if (MUT === 'gatilho-cego-novo' && nome === 'preview-bot.yml') { + texto = texto.replace(' pull_request_target:', ' pull_request_review:\n types: [submitted]\n pull_request_target:'); + } + + /* O bloco `on:` vai até a primeira chave de topo seguinte (coluna zero). Ler o + arquivo inteiro daria falso positivo com `pull_request` citado em comentário + dentro de um step. */ + const mOn = texto.match(/^on:\n([\s\S]*?)(?=^\S)/m); + if (!mOn) continue; + const bloco = mOn[1]; + const cegos = CEGOS.filter((g) => new RegExp(`^\\s{2}${g}:`, 'm').test(bloco)); + if (!cegos.length) continue; + + /* Guard de secret = o job aborta quando a variável de secret está vazia. */ + const temGuard = /-z "\$GH_TOKEN"|-z "\$\{?[A-Z_]*TOKEN/.test(texto) && /exit 1/.test(texto); + if (!temGuard) continue; + + /* A defesa aceita: um `if:` de job que amarre o event_name ou compare o repo da + head com o repositório - as duas formas de dizer "aqui eu tenho secret". */ + const temDefesa = /if:[\s\S]{0,400}?(github\.event_name\s*==|head\.repo\.full_name\s*==)/.test(texto); + if (!temDefesa) { + falhas.push(`WSEC1 ${nome}: guard de secret + gatilho cego (${cegos.join(', ')}) sem \`if:\` que exclua o caso de fork`); + } +} + +for (const f of falhas) console.log(` \x1b[31m✗\x1b[0m ${f}`); +if (!falhas.length) console.log(` \x1b[32m✓\x1b[0m WSEC1 nenhum portão reprova PR de fork por secret que aquele gatilho não pode ter (${arquivos.length} workflows)`); +if (MUT && !falhas.length) { + console.log(` \x1b[31m✗\x1b[0m MUTAÇÃO '${MUT}' não acendeu nenhuma cláusula — portão cego`); + falhas.push('mutacao-cega'); // prova que não morde é vermelho, não aviso (MC1) +} +process.exit(falhas.length ? 1 : 0); From b5ac1e7d169eca458cee85db5f781a02813e1365 Mon Sep 17 00:00:00 2001 From: csbrasil-bot Date: Sat, 22 Aug 2026 04:17:52 +0000 Subject: [PATCH 04/16] chore(docs): regenera bloco derivado (autofix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rodado pelo autofix: só arquivo gerado, conferido pela lista de permissão. Signed-off-by: csbrasil-bot --- ARCH.generated.md | 8 ++++---- README.md | 10 +++++----- STATUS.md | 4 ++-- docs/docs/arquitetura.md | 4 ++-- docs/docs/comecando.md | 12 ++++++------ docs/docs/quality-gates.md | 2 +- docs/docs/stack.md | 2 +- .../current/arquitetura.md | 4 ++-- .../current/comecando.md | 12 ++++++------ .../current/quality-gates.md | 2 +- .../docusaurus-plugin-content-docs/current/stack.md | 2 +- 11 files changed, 31 insertions(+), 31 deletions(-) diff --git a/ARCH.generated.md b/ARCH.generated.md index c86ffe0d..6d9b26d9 100644 --- a/ARCH.generated.md +++ b/ARCH.generated.md @@ -8,9 +8,9 @@ Números atuais das zonas, do quality gate e do `package.json`: | Zona | O que é | Tamanho medido | Regra | |---|---|---|---| -| `public/` | o **jogo** | 44 arquivos `.js`, 31.956 linhas · Three.js `r160` vendorizado | ES modules servidos crus, **zero build**, sem dependência de runtime | +| `public/` | o **jogo** | 44 arquivos `.js`, 31.982 linhas · Three.js `r160` vendorizado | ES modules servidos crus, **zero build**, sem dependência de runtime | | `src/` | o **site** | 18 páginas `.astro`, 21 rotas `/api` · Astro `^7.1.1` | framework é bem-vindo; `service_role` só no servidor | -| `tools/` | o **arnês** | 194 scripts em `tools/eval/`, 54 em `tools/` | node puro: sobe o jogo real sem browser | +| `tools/` | o **arnês** | 200 scripts em `tools/eval/`, 54 em `tools/` | node puro: sobe o jogo real sem browser | **Não existe `public/index.html`.** O HTML do jogo é `src/pages/index.astro`, servido na rota `/`. Servir `public/` estaticamente entrega os arnêses visuais, **não o jogo** — é a pegadinha que custa a primeira hora de todo mundo. @@ -25,10 +25,10 @@ Comandos e scripts atuais do quality gate: ```bash -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:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam +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` tem **118 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. +`package.json` tem **124 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `node -p "Object.keys(require('./package.json').scripts)"` diff --git a/README.md b/README.md index 2d4c7b72..8caa958b 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 31.956 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | -| `game.js` | **6.894** linhas | `wc -l public/js/game.js` | +| Código do jogo | 31.982 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **6.920** linhas | `wc -l public/js/game.js` | | `main.js` | 2.680 linhas | `wc -l public/js/main.js` | | Armas com GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | | GLBs de personagem | 45 | `git ls-files 'public/models/characters/*.glb' \| wc -l` | @@ -44,10 +44,10 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | Personagens jogáveis | 44, em 5 facções | array `CHARACTERS` de `characters.js` | | Mapas no registro | 12 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 194 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 200 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | -| Versão | `2.0.0-alpha.172` | `public/js/version.js` e `package.json` (batem) | +| Versão | `2.0.0-alpha.172` | `public/js/version.js` — **DIVERGE do `package.json`: `2.0.0-alpha.176`** | > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `o comando da coluna direita de cada linha` @@ -100,7 +100,7 @@ arquitetura): `cd docs && npm install && npm start` → Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `dependencies/devDependencies do package.json · REVISION de public/vendor/three.module.js` diff --git a/STATUS.md b/STATUS.md index 793a6618..bc6e9965 100644 --- a/STATUS.md +++ b/STATUS.md @@ -4,8 +4,8 @@ - **Versão:** `2.0.0-alpha.172` - **Conteúdo jogável:** 5 facções, 44 personagens, 12 mapas e 26 armas com GLB -- **Código do jogo:** 31.956 linhas em 44 módulos JavaScript -- **Automação:** 118 comandos npm, 194 scripts de avaliação e 54 scripts de pipeline +- **Código do jogo:** 31.982 linhas em 44 módulos JavaScript +- **Automação:** 124 comandos npm, 200 scripts de avaliação e 54 scripts de pipeline > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `package.json · CHARACTERS · MAPS · public/models/weapons · public/js · tools/` diff --git a/docs/docs/arquitetura.md b/docs/docs/arquitetura.md index 8dca2766..c85bb9a3 100644 --- a/docs/docs/arquitetura.md +++ b/docs/docs/arquitetura.md @@ -62,7 +62,7 @@ Tamanho dos arquivos que o `gen-arch.mjs` indexa — bloco gerado, regenerado po | Arquivo | Linhas | |---|---:| -| `public/js/game.js` | 6.894 | +| `public/js/game.js` | 6.920 | | `public/js/main.js` | 2.680 | | `public/js/characters.js` | 1.068 | | `public/js/glbchars.js` | 837 | @@ -70,7 +70,7 @@ Tamanho dos arquivos que o `gen-arch.mjs` indexa — bloco gerado, regenerado po | `public/js/weapons.js` | 344 | | `public/js/springs.js` | 260 | -Total de `public/js/`: **31.956 linhas em 44 arquivos**. O índice símbolo→linha, com a tabela de conflito, é outro bloco gerado: `tools/eval/ARCH.md` (`npm run arch`). +Total de `public/js/`: **31.982 linhas em 44 arquivos**. O índice símbolo→linha, com a tabela de conflito, é outro bloco gerado: `tools/eval/ARCH.md` (`npm run arch`). > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: ``git ls-files public/js/*.js | xargs wc -l`` diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index e7dfbb37..a32b1438 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -39,8 +39,8 @@ esta página envelhecia no primeiro commit — ver | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 31.956 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | -| `game.js` | **6.894** linhas | `wc -l public/js/game.js` | +| Código do jogo | 31.982 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **6.920** linhas | `wc -l public/js/game.js` | | `main.js` | 2.680 linhas | `wc -l public/js/main.js` | | Armas com GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | | GLBs de personagem | 45 | `git ls-files 'public/models/characters/*.glb' \| wc -l` | @@ -49,10 +49,10 @@ esta página envelhecia no primeiro commit — ver | Personagens jogáveis | 44, em 5 facções | array `CHARACTERS` de `characters.js` | | Mapas no registro | 12 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 194 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 200 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | -| Versão | `2.0.0-alpha.172` | `public/js/version.js` e `package.json` (batem) | +| Versão | `2.0.0-alpha.172` | `public/js/version.js` — **DIVERGE do `package.json`: `2.0.0-alpha.176`** | > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `o comando da coluna direita de cada linha` @@ -275,10 +275,10 @@ E os dois quality gates, com a lista exata do que cada um roda — direto do `pa {/* BEGIN:GERADO:scripts — não edite à mão, rode `npm run docs` */} ```bash -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:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam +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` tem **118 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. +`package.json` tem **124 scripts**; o motivo de cada um mora em `SCRIPTS.md` (migrado das chaves `//nome` em 18/08/2026) — é onde está o porquê. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `node -p "Object.keys(require('./package.json').scripts)"` diff --git a/docs/docs/quality-gates.md b/docs/docs/quality-gates.md index b702f226..e1fad825 100644 --- a/docs/docs/quality-gates.md +++ b/docs/docs/quality-gates.md @@ -15,7 +15,7 @@ PR (`.github/workflows/ci.yml`). {/* BEGIN:GERADO:invariantes — não edite à mão, rode `npm run docs` */} - `tools/eval/invariants.mjs`: **2.275 linhas**, **65 identificadores de invariante declarados** (`put()`), dos quais **28** têm caminho de `skip()` declarado. -- O arnês inteiro são **194 scripts** em `tools/eval/` (`.mjs` + `.py`), mais **54 scripts** de pipeline em `tools/`. +- O arnês inteiro são **200 scripts** em `tools/eval/` (`.mjs` + `.py`), mais **54 scripts** de pipeline em `tools/`. - Quantas invariantes rodam como **críticas** numa execução **não é derivável do fonte**: depende de qual insumo existe na máquina (o JSON do auditor de viewmodel, um GLB, uma pasta de anims). Esse número só sai rodando o quality gate — e o lugar dele é o cabeçalho do `KNOWN-BUGS.md`, atualizado com saída real. Reproduza: diff --git a/docs/docs/stack.md b/docs/docs/stack.md index 0073e630..2ef003fa 100644 --- a/docs/docs/stack.md +++ b/docs/docs/stack.md @@ -28,7 +28,7 @@ a partir do `package.json`, do `docs/package.json` e do próprio Three.js vendor | Esta documentação | **Docusaurus** | `3.6.3` | | Runtime de CI | **Node** | `22` | -Three.js sai de `public/vendor/three.module.js` (**sem CDN, sem npm no runtime**). Astro e Vercel de `package.json` + `astro.config.mjs` + `vercel.json`. Dos scripts de `tools/`, **109** importam Playwright, **37** importam gltf-transform e **4** importam meshoptimizer. +Three.js sai de `public/vendor/three.module.js` (**sem CDN, sem npm no runtime**). Astro e Vercel de `package.json` + `astro.config.mjs` + `vercel.json`. Dos scripts de `tools/`, **110** importam Playwright, **37** importam gltf-transform e **4** importam meshoptimizer. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `dependencies/devDependencies do package.json · REVISION de public/vendor/three.module.js` diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md index 2da28780..a09aef99 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md @@ -69,7 +69,7 @@ Size of the files `gen-arch.mjs` indexes — a generated block, regenerated by | File | Lines | |---|---:| -| `public/js/game.js` | 6,894 | +| `public/js/game.js` | 6,920 | | `public/js/main.js` | 2,680 | | `public/js/characters.js` | 1,068 | | `public/js/glbchars.js` | 837 | @@ -77,7 +77,7 @@ Size of the files `gen-arch.mjs` indexes — a generated block, regenerated by | `public/js/weapons.js` | 344 | | `public/js/springs.js` | 260 | -Total in `public/js/`: **31,956 lines in 44 files**. The symbol-to-line index lives in `tools/eval/ARCH.md`. +Total in `public/js/`: **31,982 lines in 44 files**. The symbol-to-line index lives in `tools/eval/ARCH.md`. > Block generated by `node tools/gen-docs.mjs`. Source: ``wc -l public/js/*.js`` diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md index dedea8b7..7fde13ac 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md @@ -41,8 +41,8 @@ this page was aging at the very first commit — see | What | How much | Where to check | |---|---:|---| -| Game code | 31,956 lines in 44 files | `git ls-files public/js/*.js \| xargs wc -l` | -| `game.js` | **6,894** lines | `wc -l public/js/game.js` | +| Game code | 31,982 lines in 44 files | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **6,920** lines | `wc -l public/js/game.js` | | `main.js` | 2,680 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` | @@ -51,10 +51,10 @@ this page was aging at the very first commit — see | 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 | 194 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Harness scripts | 200 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Pipeline scripts | 54 | `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.172` | `public/js/version.js` and `package.json` (match) | +| Version | `2.0.0-alpha.172` | `public/js/version.js` and `package.json` **DIVERGE** | > Block generated by `node tools/gen-docs.mjs`. Source: `the command in the right column of each row` @@ -277,10 +277,10 @@ And the two gates, with the exact list of what each one runs — straight from ` {/* BEGIN:GERADO:scripts — não edite à mão, rode `npm run docs` */} ```bash -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:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam +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 **118 scripts**; the reason behind each one lives in `SCRIPTS.md`. +`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)"` diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md index c2f45180..53b1f97a 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/quality-gates.md @@ -18,7 +18,7 @@ PR (`.github/workflows/ci.yml`). {/* BEGIN:GERADO:invariantes — não edite à mão, rode `npm run docs` */} - `tools/eval/invariants.mjs`: **2,275 lines**, **65 declared invariant identifiers**, with **28** declared `skip()` paths. -- The harness contains **194 scripts** in `tools/eval/`, plus **54 pipeline scripts** in `tools/`. +- The harness contains **200 scripts** in `tools/eval/`, plus **54 pipeline scripts** in `tools/`. - The number of critical checks in one run depends on the inputs present on that machine; dated results belong in `KNOWN-BUGS.md`. ```bash diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/stack.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/stack.md index 32511126..fe9e9753 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/stack.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/stack.md @@ -31,7 +31,7 @@ from `package.json`, `docs/package.json` and the vendored Three.js itself. | This documentation | **Docusaurus** | `3.6.3` | | CI runtime | **Node** | `22` | -Three.js comes from `public/vendor/three.module.js`. Of the scripts in `tools/`, **109** import Playwright, **37** import gltf-transform, and **4** import meshoptimizer. +Three.js comes from `public/vendor/three.module.js`. Of the scripts in `tools/`, **110** import Playwright, **37** import gltf-transform, and **4** import meshoptimizer. > Block generated by `node tools/gen-docs.mjs`. Source: `dependencies/devDependencies do package.json · REVISION de public/vendor/three.module.js` From 97c1ab7348a3f80fad4a4b970e90ded5578e7463 Mon Sep 17 00:00:00 2001 From: csbrasil-bot Date: Sat, 22 Aug 2026 04:18:24 +0000 Subject: [PATCH 05/16] chore(docs): regenera bloco derivado (autofix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rodado pelo autofix: só arquivo gerado, conferido pela lista de permissão. Signed-off-by: csbrasil-bot --- ARCH.generated.md | 2 +- README.md | 4 ++-- STATUS.md | 2 +- docs/docs/arquitetura.md | 4 ++-- docs/docs/comecando.md | 4 ++-- .../en/docusaurus-plugin-content-docs/current/arquitetura.md | 4 ++-- .../en/docusaurus-plugin-content-docs/current/comecando.md | 4 ++-- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ARCH.generated.md b/ARCH.generated.md index e23771a0..f8bd4d28 100644 --- a/ARCH.generated.md +++ b/ARCH.generated.md @@ -8,7 +8,7 @@ Números atuais das zonas, do quality gate e do `package.json`: | Zona | O que é | Tamanho medido | Regra | |---|---|---|---| -| `public/` | o **jogo** | 44 arquivos `.js`, 32.001 linhas · Three.js `r160` vendorizado | ES modules servidos crus, **zero build**, sem dependência de runtime | +| `public/` | o **jogo** | 44 arquivos `.js`, 32.027 linhas · Three.js `r160` vendorizado | ES modules servidos crus, **zero build**, sem dependência de runtime | | `src/` | o **site** | 18 páginas `.astro`, 21 rotas `/api` · Astro `^7.1.1` | framework é bem-vindo; `service_role` só no servidor | | `tools/` | o **arnês** | 200 scripts em `tools/eval/`, 54 em `tools/` | node puro: sobe o jogo real sem browser | diff --git a/README.md b/README.md index ac9728f2..98942d3f 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 32.001 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | -| `game.js` | **6.910** linhas | `wc -l public/js/game.js` | +| Código do jogo | 32.027 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **6.936** linhas | `wc -l public/js/game.js` | | `main.js` | 2.698 linhas | `wc -l public/js/main.js` | | Armas com GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | | GLBs de personagem | 45 | `git ls-files 'public/models/characters/*.glb' \| wc -l` | diff --git a/STATUS.md b/STATUS.md index f24bbc7e..1314890a 100644 --- a/STATUS.md +++ b/STATUS.md @@ -4,7 +4,7 @@ - **Versão:** `2.0.0-alpha.176` - **Conteúdo jogável:** 5 facções, 44 personagens, 12 mapas e 26 armas com GLB -- **Código do jogo:** 32.001 linhas em 44 módulos JavaScript +- **Código do jogo:** 32.027 linhas em 44 módulos JavaScript - **Automação:** 124 comandos npm, 200 scripts de avaliação e 54 scripts de pipeline > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `package.json · CHARACTERS · MAPS · public/models/weapons · public/js · tools/` diff --git a/docs/docs/arquitetura.md b/docs/docs/arquitetura.md index be541632..803f9da6 100644 --- a/docs/docs/arquitetura.md +++ b/docs/docs/arquitetura.md @@ -62,7 +62,7 @@ Tamanho dos arquivos que o `gen-arch.mjs` indexa — bloco gerado, regenerado po | Arquivo | Linhas | |---|---:| -| `public/js/game.js` | 6.910 | +| `public/js/game.js` | 6.936 | | `public/js/main.js` | 2.698 | | `public/js/characters.js` | 1.068 | | `public/js/glbchars.js` | 844 | @@ -70,7 +70,7 @@ Tamanho dos arquivos que o `gen-arch.mjs` indexa — bloco gerado, regenerado po | `public/js/weapons.js` | 346 | | `public/js/springs.js` | 260 | -Total de `public/js/`: **32.001 linhas em 44 arquivos**. O índice símbolo→linha, com a tabela de conflito, é outro bloco gerado: `tools/eval/ARCH.md` (`npm run arch`). +Total de `public/js/`: **32.027 linhas em 44 arquivos**. O índice símbolo→linha, com a tabela de conflito, é outro bloco gerado: `tools/eval/ARCH.md` (`npm run arch`). > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: ``git ls-files public/js/*.js | xargs wc -l`` diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index 2136266b..f331348d 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -39,8 +39,8 @@ esta página envelhecia no primeiro commit — ver | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 32.001 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | -| `game.js` | **6.910** linhas | `wc -l public/js/game.js` | +| Código do jogo | 32.027 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **6.936** linhas | `wc -l public/js/game.js` | | `main.js` | 2.698 linhas | `wc -l public/js/main.js` | | Armas com GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | | GLBs de personagem | 45 | `git ls-files 'public/models/characters/*.glb' \| wc -l` | diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md index bf263a22..302c92fd 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md @@ -69,7 +69,7 @@ Size of the files `gen-arch.mjs` indexes — a generated block, regenerated by | File | Lines | |---|---:| -| `public/js/game.js` | 6,910 | +| `public/js/game.js` | 6,936 | | `public/js/main.js` | 2,698 | | `public/js/characters.js` | 1,068 | | `public/js/glbchars.js` | 844 | @@ -77,7 +77,7 @@ Size of the files `gen-arch.mjs` indexes — a generated block, regenerated by | `public/js/weapons.js` | 346 | | `public/js/springs.js` | 260 | -Total in `public/js/`: **32,001 lines in 44 files**. The symbol-to-line index lives in `tools/eval/ARCH.md`. +Total in `public/js/`: **32,027 lines in 44 files**. The symbol-to-line index lives in `tools/eval/ARCH.md`. > Block generated by `node tools/gen-docs.mjs`. Source: ``wc -l public/js/*.js`` diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md index b724aaa4..4234e07b 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md @@ -41,8 +41,8 @@ this page was aging at the very first commit — see | 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` | +| Game code | 32,027 lines in 44 files | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **6,936** 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` | From 5aa678bcc9252f8da965316c6229a070000603eb Mon Sep 17 00:00:00 2001 From: csbrasil-bot Date: Sat, 22 Aug 2026 04:19:14 +0000 Subject: [PATCH 06/16] =?UTF-8?q?Merge=20da=20base=20(conflito=20s=C3=B3?= =?UTF-8?q?=20em=20arquivo=20gerado,=20resolvido=20pelo=20autofix)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent: csbrasil-bot (autofix) Signed-off-by: csbrasil-bot --- package-lock.json | 4 ++-- package.json | 2 +- tools/eval/ARCH.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index e2556455..3d6977d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "coro-solto", - "version": "2.0.0-alpha.176", + "version": "2.0.0-alpha.177", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "coro-solto", - "version": "2.0.0-alpha.176", + "version": "2.0.0-alpha.177", "license": "AGPL-3.0-only", "dependencies": { "@astrojs/vercel": "^11.0.6", diff --git a/package.json b/package.json index 29eebdad..c574a47b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coro-solto", - "version": "2.0.0-alpha.176", + "version": "2.0.0-alpha.177", "license": "AGPL-3.0-only", "description": "CORO SOLTO: Treta Suprema (ex-CS BRASIL) — FPS satírico de navegador com 5 facções brasileiras (site Astro + jogo vanilla em Three.js), gerado em par com agentes de IA (AI generated & AI friendly)", "scripts": { diff --git a/tools/eval/ARCH.md b/tools/eval/ARCH.md index e8d932ec..cb96bc69 100644 --- a/tools/eval/ARCH.md +++ b/tools/eval/ARCH.md @@ -3,7 +3,7 @@ > Gerado por `node tools/gen-arch.mjs`. **Não edite este bloco à mão.** -> Versão do jogo: 2.0.0-alpha.176 · `npm run arch` para regenerar · `npm run arch:check` no CI. +> Versão do jogo: 2.0.0-alpha.177 · `npm run arch` para regenerar · `npm run arch:check` no CI. ## Tamanho dos arquivos indexados From 10c83b060285e844d99ebc2f05327508dda1554f Mon Sep 17 00:00:00 2001 From: csbrasil-bot Date: Sat, 22 Aug 2026 04:19:14 +0000 Subject: [PATCH 07/16] chore(docs): regenera bloco derivado (autofix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rodado pelo autofix: só arquivo gerado, conferido pela lista de permissão. Signed-off-by: csbrasil-bot --- README.md | 2 +- docs/docs/comecando.md | 2 +- .../i18n/en/docusaurus-plugin-content-docs/current/comecando.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 98942d3f..81c77b8e 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | Scripts do arnês | 200 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | -| Versão | `2.0.0-alpha.176` | `public/js/version.js` e `package.json` (batem) | +| Versão | `2.0.0-alpha.176` | `public/js/version.js` — **DIVERGE do `package.json`: `2.0.0-alpha.177`** | > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `o comando da coluna direita de cada linha` diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index f331348d..69a6b41d 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -52,7 +52,7 @@ esta página envelhecia no primeiro commit — ver | Scripts do arnês | 200 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | -| Versão | `2.0.0-alpha.176` | `public/js/version.js` e `package.json` (batem) | +| Versão | `2.0.0-alpha.176` | `public/js/version.js` — **DIVERGE do `package.json`: `2.0.0-alpha.177`** | > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `o comando da coluna direita de cada linha` diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md index 4234e07b..03240c53 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md @@ -54,7 +54,7 @@ this page was aging at the very first commit — see | Harness scripts | 200 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Pipeline scripts | 54 | `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.176` | `public/js/version.js` and `package.json` (match) | +| Version | `2.0.0-alpha.176` | `public/js/version.js` and `package.json` **DIVERGE** | > Block generated by `node tools/gen-docs.mjs`. Source: `the command in the right column of each row` From e3b55222a5e081c386c71abe640335464c164584 Mon Sep 17 00:00:00 2001 From: csbrasil-bot Date: Sat, 22 Aug 2026 04:19:46 +0000 Subject: [PATCH 08/16] chore(docs): regenera bloco derivado (autofix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rodado pelo autofix: só arquivo gerado, conferido pela lista de permissão. Signed-off-by: csbrasil-bot --- README.md | 4 ++-- docs/docs/colaborar.md | 2 +- docs/docs/comecando.md | 4 ++-- .../en/docusaurus-plugin-content-docs/current/colaborar.md | 2 +- .../en/docusaurus-plugin-content-docs/current/comecando.md | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 391db7ab..63334daf 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 32.001 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | -| `game.js` | **6.910** linhas | `wc -l public/js/game.js` | +| Código do jogo | 32.027 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **6.936** linhas | `wc -l public/js/game.js` | | `main.js` | 2.698 linhas | `wc -l public/js/main.js` | | Armas com GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | | GLBs de personagem | 45 | `git ls-files 'public/models/characters/*.glb' \| wc -l` | diff --git a/docs/docs/colaborar.md b/docs/docs/colaborar.md index 2d5b3e07..1f0322ef 100644 --- a/docs/docs/colaborar.md +++ b/docs/docs/colaborar.md @@ -14,7 +14,7 @@ O número abaixo não é retórica, e não é escrito à mão: sai de `git short {/* BEGIN:GERADO:pessoas — não edite à mão, rode `npm run docs` */} -**11 identidades de autoria humana** assinam commit no histórico **desta branch**: `ruben-cytonic`, `Ruben`, `Emerson Garrido`, `rubenmarcus`, `Ruben Marcus`, `William Oliveira`, `Juan Versolato Lopes`, `daeeseD`, `matheusgb`, `Maná Soares`, `daltonfontes`. O resto dos commits é assinado por agentes de IA. Branch não é repositório: quem contribuiu num ramo que esta branch não contém **não aparece aqui**. +**11 identidades de autoria humana** assinam commit no histórico **desta branch**: `ruben-cytonic`, `Emerson Garrido`, `Ruben`, `rubenmarcus`, `Ruben Marcus`, `William Oliveira`, `Juan Versolato Lopes`, `daeeseD`, `matheusgb`, `Maná Soares`, `daltonfontes`. O resto dos commits é assinado por agentes de IA. Branch não é repositório: quem contribuiu num ramo que esta branch não contém **não aparece aqui**. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `git shortlog -sn --no-merges (descontando autores que são agentes)` diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index 8c9d253c..1beacbb5 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -39,8 +39,8 @@ esta página envelhecia no primeiro commit — ver | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 32.001 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | -| `game.js` | **6.910** linhas | `wc -l public/js/game.js` | +| Código do jogo | 32.027 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **6.936** linhas | `wc -l public/js/game.js` | | `main.js` | 2.698 linhas | `wc -l public/js/main.js` | | Armas com GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | | GLBs de personagem | 45 | `git ls-files 'public/models/characters/*.glb' \| wc -l` | diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/colaborar.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/colaborar.md index ad72a3a1..2106b06f 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/colaborar.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/colaborar.md @@ -17,7 +17,7 @@ The number below is not rhetoric, and it is not hand-written: it comes from `git {/* BEGIN:GERADO:pessoas — não edite à mão, rode `npm run docs` */} -**11 human author identities** sign commits in this branch: `ruben-cytonic`, `Ruben`, `Emerson Garrido`, `rubenmarcus`, `Ruben Marcus`, `William Oliveira`, `Juan Versolato Lopes`, `daeeseD`, `matheusgb`, `Maná Soares`, `daltonfontes`. Automated identities are excluded. A Git author name is not necessarily one unique person. +**11 human author identities** sign commits in this branch: `ruben-cytonic`, `Emerson Garrido`, `Ruben`, `rubenmarcus`, `Ruben Marcus`, `William Oliveira`, `Juan Versolato Lopes`, `daeeseD`, `matheusgb`, `Maná Soares`, `daltonfontes`. Automated identities are excluded. A Git author name is not necessarily one unique person. > Block generated by `node tools/gen-docs.mjs`. Source: `git shortlog -sn --no-merges (descontando autores que são agentes)` diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md index 888a3d9a..2108ba1e 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md @@ -41,8 +41,8 @@ this page was aging at the very first commit — see | 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` | +| Game code | 32,027 lines in 44 files | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **6,936** 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` | From fc7104697e66635e48b8eb709e31c22c716f4d11 Mon Sep 17 00:00:00 2001 From: csbrasil-bot Date: Sat, 22 Aug 2026 05:50:06 +0000 Subject: [PATCH 09/16] =?UTF-8?q?Merge=20da=20base=20(conflito=20s=C3=B3?= =?UTF-8?q?=20em=20arquivo=20gerado,=20resolvido=20pelo=20autofix)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent: csbrasil-bot (autofix) Signed-off-by: csbrasil-bot --- tools/eval/autofix-check.mjs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tools/eval/autofix-check.mjs b/tools/eval/autofix-check.mjs index ec57c443..c60da58a 100644 --- a/tools/eval/autofix-check.mjs +++ b/tools/eval/autofix-check.mjs @@ -17,6 +17,10 @@ workflow pode chamar `gh pr merge`. O `csbrasil-bot-automerge` chamava, e era a única coisa que um bot daqui fazia sozinho — justamente a que não devia. Ele agora aplica `pronto-pra-merge` e o botão continua humano. + AF7 · todo commit que o bot faz carrega os trailers que o CI cobra de humano: + `Agent:` (agente_check) e `Signed-off-by:` (dco_check). Sem eles o bot conserta + o PR e reprova o mesmo PR no passo seguinte - foi o que aconteceu no primeiro + disparo real, em 22/08. AF6 · o autofix acorda quando a MAIN anda, não só quando o PR se mexe. Foram 9 releases em 20 horas e cada um reabre conflito em todo PR aberto; sem o gatilho de push o bot só conserta quem empurra commit — ou seja, nunca @@ -40,6 +44,7 @@ const MUTANTES = { 'automerge-volta': 'AF4', 'resolve-conflito-de-codigo': 'AF5', 'sem-varredura-pos-release': 'AF6', + 'commit-sem-trailer': 'AF7', }; if (MUT && !MUTANTES[MUT]) { console.error(`mutante desconhecido: ${MUT}`); process.exit(2); } @@ -109,6 +114,14 @@ if (resolveConflito) { } } +/* ---- AF7: os commits do bot levam os trailers que o CI cobra ---- */ +if (MUT === 'commit-sem-trailer') wf = wf.replace(/\n\s*-m "Agent: csbrasil-bot \(autofix\)" \\/g, ''); +const commits = [...wf.matchAll(/git commit[\s\S]{0,600}?(?=\n\s{0,10}[a-z-]+:|\n\s*$)/g)].map((m) => m[0]); +for (const c of commits) { + if (!/Agent:/.test(c)) falhas.push('AF7 um `git commit` do bot não leva o trailer `Agent:` — o agente_check reprova o PR que ele acabou de consertar'); + if (!/Signed-off-by:/.test(c)) falhas.push('AF7 um `git commit` do bot não leva `Signed-off-by:` — o dco reprova o PR que ele acabou de consertar'); +} + /* ---- AF6: a varredura pós-release existe e enxerga quem ficou para trás ---- */ if (MUT === 'sem-varredura-pos-release') wf = wf.replace(/\n push:\n branches: \[main\]/, ''); const temPush = /^\s{2}push:\n\s{4}branches: \[main\]/m.test(wf); From 3227dc10de3c61a8c21548a70196d82d0b0407e4 Mon Sep 17 00:00:00 2001 From: csbrasil-bot Date: Sat, 22 Aug 2026 05:51:04 +0000 Subject: [PATCH 10/16] =?UTF-8?q?Merge=20da=20base=20(conflito=20s=C3=B3?= =?UTF-8?q?=20em=20arquivo=20gerado,=20resolvido=20pelo=20autofix)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent: csbrasil-bot (autofix) Signed-off-by: csbrasil-bot --- package-lock.json | 4 ++-- package.json | 2 +- tools/eval/ARCH.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3d6977d7..30c2652b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "coro-solto", - "version": "2.0.0-alpha.177", + "version": "2.0.0-alpha.178", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "coro-solto", - "version": "2.0.0-alpha.177", + "version": "2.0.0-alpha.178", "license": "AGPL-3.0-only", "dependencies": { "@astrojs/vercel": "^11.0.6", diff --git a/package.json b/package.json index c574a47b..efd43523 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coro-solto", - "version": "2.0.0-alpha.177", + "version": "2.0.0-alpha.178", "license": "AGPL-3.0-only", "description": "CORO SOLTO: Treta Suprema (ex-CS BRASIL) — FPS satírico de navegador com 5 facções brasileiras (site Astro + jogo vanilla em Three.js), gerado em par com agentes de IA (AI generated & AI friendly)", "scripts": { diff --git a/tools/eval/ARCH.md b/tools/eval/ARCH.md index cb96bc69..d1020873 100644 --- a/tools/eval/ARCH.md +++ b/tools/eval/ARCH.md @@ -3,7 +3,7 @@ > Gerado por `node tools/gen-arch.mjs`. **Não edite este bloco à mão.** -> Versão do jogo: 2.0.0-alpha.177 · `npm run arch` para regenerar · `npm run arch:check` no CI. +> Versão do jogo: 2.0.0-alpha.178 · `npm run arch` para regenerar · `npm run arch:check` no CI. ## Tamanho dos arquivos indexados From 71e35dc92244d42139c4275fedcd59d353532ab7 Mon Sep 17 00:00:00 2001 From: csbrasil-bot Date: Sat, 22 Aug 2026 05:51:04 +0000 Subject: [PATCH 11/16] chore(docs): regenera bloco derivado (autofix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rodado pelo autofix: só arquivo gerado, conferido pela lista de permissão. Agent: csbrasil-bot (autofix) Signed-off-by: csbrasil-bot --- README.md | 2 +- docs/docs/comecando.md | 2 +- .../i18n/en/docusaurus-plugin-content-docs/current/comecando.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 63334daf..e6a73152 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | Scripts do arnês | 200 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | -| Versão | `2.0.0-alpha.177` | `public/js/version.js` e `package.json` (batem) | +| Versão | `2.0.0-alpha.177` | `public/js/version.js` — **DIVERGE do `package.json`: `2.0.0-alpha.178`** | > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `o comando da coluna direita de cada linha` diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index 1beacbb5..fe226125 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -52,7 +52,7 @@ esta página envelhecia no primeiro commit — ver | Scripts do arnês | 200 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 54 | `git ls-files 'tools/*.mjs' \| wc -l` | | Tarefas de entrada escritas | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | -| Versão | `2.0.0-alpha.177` | `public/js/version.js` e `package.json` (batem) | +| Versão | `2.0.0-alpha.177` | `public/js/version.js` — **DIVERGE do `package.json`: `2.0.0-alpha.178`** | > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `o comando da coluna direita de cada linha` diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md index 2108ba1e..63cdd0e4 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md @@ -54,7 +54,7 @@ this page was aging at the very first commit — see | Harness scripts | 200 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Pipeline scripts | 54 | `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.177` | `public/js/version.js` and `package.json` (match) | +| Version | `2.0.0-alpha.177` | `public/js/version.js` and `package.json` **DIVERGE** | > Block generated by `node tools/gen-docs.mjs`. Source: `the command in the right column of each row` From c3495de7ac9b32317a9b34fbc7de07d1beedb829 Mon Sep 17 00:00:00 2001 From: csbrasil-bot Date: Sat, 22 Aug 2026 05:51:42 +0000 Subject: [PATCH 12/16] chore(docs): regenera bloco derivado (autofix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rodado pelo autofix: só arquivo gerado, conferido pela lista de permissão. Agent: csbrasil-bot (autofix) Signed-off-by: csbrasil-bot --- README.md | 4 ++-- docs/docs/comecando.md | 4 ++-- .../en/docusaurus-plugin-content-docs/current/comecando.md | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f24151e7..5ae4f365 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 32.001 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | -| `game.js` | **6.910** linhas | `wc -l public/js/game.js` | +| Código do jogo | 32.027 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **6.936** linhas | `wc -l public/js/game.js` | | `main.js` | 2.698 linhas | `wc -l public/js/main.js` | | Armas com GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | | GLBs de personagem | 45 | `git ls-files 'public/models/characters/*.glb' \| wc -l` | diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index 60a11d29..99937dec 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -39,8 +39,8 @@ esta página envelhecia no primeiro commit — ver | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 32.001 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | -| `game.js` | **6.910** linhas | `wc -l public/js/game.js` | +| Código do jogo | 32.027 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **6.936** linhas | `wc -l public/js/game.js` | | `main.js` | 2.698 linhas | `wc -l public/js/main.js` | | Armas com GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | | GLBs de personagem | 45 | `git ls-files 'public/models/characters/*.glb' \| wc -l` | diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md index 18eac13a..1a2cef88 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md @@ -41,8 +41,8 @@ this page was aging at the very first commit — see | 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` | +| Game code | 32,027 lines in 44 files | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **6,936** 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` | From 716cab42e82ca1c10d06bb422b712f9da8d8d663 Mon Sep 17 00:00:00 2001 From: csbrasil-bot Date: Sat, 22 Aug 2026 07:10:25 +0000 Subject: [PATCH 13/16] chore(docs): regenera bloco derivado (autofix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rodado pelo autofix: só arquivo gerado, conferido pela lista de permissão. Signed-off-by: csbrasil-bot --- docs/docs/colaborar.md | 2 +- .../i18n/en/docusaurus-plugin-content-docs/current/colaborar.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/colaborar.md b/docs/docs/colaborar.md index 1f0322ef..2d5b3e07 100644 --- a/docs/docs/colaborar.md +++ b/docs/docs/colaborar.md @@ -14,7 +14,7 @@ O número abaixo não é retórica, e não é escrito à mão: sai de `git short {/* BEGIN:GERADO:pessoas — não edite à mão, rode `npm run docs` */} -**11 identidades de autoria humana** assinam commit no histórico **desta branch**: `ruben-cytonic`, `Emerson Garrido`, `Ruben`, `rubenmarcus`, `Ruben Marcus`, `William Oliveira`, `Juan Versolato Lopes`, `daeeseD`, `matheusgb`, `Maná Soares`, `daltonfontes`. O resto dos commits é assinado por agentes de IA. Branch não é repositório: quem contribuiu num ramo que esta branch não contém **não aparece aqui**. +**11 identidades de autoria humana** assinam commit no histórico **desta branch**: `ruben-cytonic`, `Ruben`, `Emerson Garrido`, `rubenmarcus`, `Ruben Marcus`, `William Oliveira`, `Juan Versolato Lopes`, `daeeseD`, `matheusgb`, `Maná Soares`, `daltonfontes`. O resto dos commits é assinado por agentes de IA. Branch não é repositório: quem contribuiu num ramo que esta branch não contém **não aparece aqui**. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `git shortlog -sn --no-merges (descontando autores que são agentes)` diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/colaborar.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/colaborar.md index 2106b06f..ad72a3a1 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/colaborar.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/colaborar.md @@ -17,7 +17,7 @@ The number below is not rhetoric, and it is not hand-written: it comes from `git {/* BEGIN:GERADO:pessoas — não edite à mão, rode `npm run docs` */} -**11 human author identities** sign commits in this branch: `ruben-cytonic`, `Emerson Garrido`, `Ruben`, `rubenmarcus`, `Ruben Marcus`, `William Oliveira`, `Juan Versolato Lopes`, `daeeseD`, `matheusgb`, `Maná Soares`, `daltonfontes`. Automated identities are excluded. A Git author name is not necessarily one unique person. +**11 human author identities** sign commits in this branch: `ruben-cytonic`, `Ruben`, `Emerson Garrido`, `rubenmarcus`, `Ruben Marcus`, `William Oliveira`, `Juan Versolato Lopes`, `daeeseD`, `matheusgb`, `Maná Soares`, `daltonfontes`. Automated identities are excluded. A Git author name is not necessarily one unique person. > Block generated by `node tools/gen-docs.mjs`. Source: `git shortlog -sn --no-merges (descontando autores que são agentes)` From daf3adfd79b277217634898038f805282296a37c Mon Sep 17 00:00:00 2001 From: csbrasil-bot Date: Sat, 22 Aug 2026 21:13:41 +0000 Subject: [PATCH 14/16] chore(docs): regenera bloco derivado (autofix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rodado pelo autofix: só arquivo gerado, conferido pela lista de permissão. Signed-off-by: csbrasil-bot --- STATUS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/STATUS.md b/STATUS.md index 1678591d..2588a30d 100644 --- a/STATUS.md +++ b/STATUS.md @@ -4,7 +4,7 @@ - **Versão:** `2.0.0-alpha.180` - **Conteúdo jogável:** 5 facções, 44 personagens, 12 mapas e 26 armas com GLB -- **Código do jogo:** 32.001 linhas em 44 módulos JavaScript +- **Código do jogo:** 32.027 linhas em 44 módulos JavaScript - **Automação:** 124 comandos npm, 201 scripts de avaliação e 55 scripts de pipeline > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `package.json · CHARACTERS · MAPS · public/models/weapons · public/js · tools/` From 052021e52bb662ea9fbf4e418648da9a36f2b936 Mon Sep 17 00:00:00 2001 From: Emerson Garrido Date: Mon, 24 Aug 2026 09:35:53 -0300 Subject: [PATCH 15/16] =?UTF-8?q?feat(camera):=20modo=20de=203=C2=AA=20pes?= =?UTF-8?q?soa=20com=20toggle=20(tecla=20B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tecla B cicla: 1ª pessoa -> 3ª SOCOM (cintura pra cima) -> ombro colado - Reusa buildCharacterModel dos bots (grip procedural: mount RightHand + IK + curl) - Corpo cai na morte (ctrl.die), visivel ate em 1ª pessoa; some no respawn - Em 3ª pessoa, tiro/fumaca saem do OLHO (_eyeWorld/_aimOrigin), nao da camera - Doc da pega de arma no Blender: docs/RIG-PEGA-ARMA.md Co-Authored-By: Claude Opus 4.8 Signed-off-by: Emerson Garrido Agent: Claude Code --- docs/RIG-PEGA-ARMA.md | 173 ++++++++++++++++++++++++++++++++++++++++++ public/js/game.js | 155 ++++++++++++++++++++++++++++++++++++- 2 files changed, 324 insertions(+), 4 deletions(-) create mode 100644 docs/RIG-PEGA-ARMA.md diff --git a/docs/RIG-PEGA-ARMA.md b/docs/RIG-PEGA-ARMA.md new file mode 100644 index 00000000..8fe0a121 --- /dev/null +++ b/docs/RIG-PEGA-ARMA.md @@ -0,0 +1,173 @@ +# Pega de arma no rig (Blender) — receita replicável + +> **Feito em:** 2026-08-23 · **Personagem-piloto:** `mandrake` · **Arma-piloto:** `pistol` +> **Objetivo:** montar, à mão no Blender, a pega correta de uma arma no rig do personagem +> (mão de lado envolvendo o cabo, cano pra frente), extrair os **números exatos** e +> deixar registrado pra replicar em outras armas e outros personagens. + +Este documento é a régua da tarefa: quem replicar num segundo par (arma/personagem) tem +que chegar nos mesmos passos e nos mesmos tipos de número. Se um passo não bater, é sinal +de que o rig do novo par diverge — e isso é informação, não erro. + +--- + +## 0. Contexto do rig (o que fez a coisa dar trabalho) + +- O corpo do personagem vem em `models/characters/.glb` (malha + textura + skin). + O **rig do jogo** tem 24 ossos (Hips/Spine/…/RightHand), **sem ossos de dedo**. +- O rig usado aqui pra AUTORAR a pega veio do `Flair.fbx` (Mixamo, 65 ossos **com dedos**: + `mixamorig:RightHandIndex1..3`, `Middle`, `Ring`, `Pinky`, `Thumb1..3`). A malha do + mandrake dentro do FBX é a **mesma** (6129 verts, UV idêntico) — por isso deu pra aplicar + o material texturizado nela e ver a pega com textura. +- **Escala:** o FBX Mixamo entra com `scale = 0.01` → o personagem fica com **1,7 cm**. + Normalizar `Armature.001.scale = 1.0` deixa ele em **1,7 m** (bate com as armas e com o + jogo). **Sempre normalize a escala antes de medir/posicionar qualquer coisa.** +- **Convenção de eixos** (personagem em pé): forward do personagem = **-Y**, up = **+Z**. + +### Gotcha crítico do viewport (custou tempo, anota) +Esconder o esqueleto pelo **`hide_viewport`** (ícone monitor) **desliga a avaliação da IK** +e **congela a deformação do mesh** numa pose antiga — parece que "perdeu a pega", mas os +dados estão certos. Regras: +1. Com rig que usa **IK**, **asse a IK em FK** antes de esconder (`pose.visual_transform_apply` + + remover as constraints de IK). +2. Pra esconder os ossos sem quebrar, use o **"olho" (`hide_set`)**, nunca o monitor. +3. Se o mesh congelar mesmo assim: **entrar e sair do Modo de Edição** do mesh força o + recálculo do modifier (destrava na hora). + +--- + +## 1. Passo a passo (o que foi feito, em ordem) + +1. **Importar** `models/characters/mandrake.glb` (corpo+textura) e o `Flair.fbx` (rig+anim). + Aplicar o material `Material_1` (texturas: `texture_0` albedo 1024, `mandrake_normal`, + `mandrake_metalrough`) na malha skinada do rig Mixamo (`char1.001`). UV é idêntico. +2. **Normalizar escala** do rig: `Armature.001.scale = (1,1,1)` → personagem 1,7 m. +3. **Pose do braço** (uma mão, pistola): alvos de **IK** (empties) nas mãos + constraint de + IK de 2 ossos em cada antebraço. + - Mão direita (do gatilho): alvo à frente, ~altura do ombro → `(-0.05, -0.42, 1.24)`. + - Mão esquerda (apoio, relaxada no quadril) → `(0.17, -0.05, 0.92)`. +4. **Roll da mão** direita: girar `mixamorig:RightHand` **+90° no eixo Y local** → a normal + da palma sai de "pra baixo" (-Z) pra "de lado, pra dentro" (+X). Sem isso a palma fica + pra baixo e a arma "deitada". +5. **Curl dos dedos** (formar o punho em volta do cabo) — rotação por falange no eixo X + local (polegar no eixo Z local). Ângulos usados (graus): + | dedo | seg1 | seg2 | seg3 | + |--------|------|------|------| + | Index | 38 | 45 | 35 | + | Middle | 62 | 70 | 55 | + | Ring | 64 | 72 | 58 | + | Pinky | 60 | 66 | 52 | + | Thumb | 28 | 34 | 26 | + > Index é o dedo do gatilho: fecha menos que os outros três. +6. **Assentar a arma** (o passo que "prende" a arma na mão): + - Medir na malha da pistola o **centro do cabo** (backstrap): vértices do terço traseiro + em X **e** metade inferior em Z → centroide `grip_hold`. + - Medir o **centro do punho**: centroide das bases dos dedos (`*1` das 4 fingers). + - Posicionar a arma de modo que `grip_hold` caia no centro do punho, cano em **-Y**, + topo do slide em **+Z**, escala pro tamanho real (`CFG.pistol.len = 0.26 m`). + - Ajuste fino pedido pelo dono: puxar a arma **+3,5 cm em Y** (pra trás, sentido oposto + ao cano) pra o cabo assentar mais fundo, encostando no "V" do polegar. +7. **Assar IK→FK** e salvar a pose como Action **`pistol_aim_R`**. + +--- + +## 2. Números extraídos (a saída reutilizável) + +> Estes são os números que replicam a pega. O **mount** é o mais importante: é o transform +> da arma no espaço LOCAL do osso da mão — mesmo formato do mount procedural do jogo +> (`glbchars.js`, `handBone.add(mount)`). + +### Mount da PISTOLA (local a `mixamorig:RightHand`) +``` +loc (m) = (0.0389, 0.1186, 0.0095) +rot (deg) = (-84.85, -8.58, -98.66) # Euler XYZ +scale = 0.2098 +``` + +### Roll da mão direita +``` +mixamorig:RightHand.rotation_quaternion = (0.7071, 0, 0.7071, 0) # 90° em Y local +``` + +### Curl dos dedos (graus) — ver tabela no passo 5. + +### Alvos de IK (mundo, personagem 1,72 m, forward -Y) +``` +RightHand target = (-0.05, -0.42, 1.24) +LeftHand target = ( 0.17, -0.05, 0.92) # apoio no quadril (pose de 1 mão) +``` + +--- + +## 3. Como replicar + +### Outra ARMA, mesmo personagem +1. Importar o GLB da arma (`models/weapons/.glb`), escalar pra `CFG..len` (weapons.js). +2. **Rifle (2 mãos):** manter o mount da mão direita no cabo e **adicionar a mão esquerda** + no guarda-mão (ponto `gripPoints(id).fore` — já existe no jogo). No Blender, um 2º alvo + de IK na mão esquerda, no guarda-mão. +3. **Pistola/revólver (1 mão):** reusar direto os números da seção 2; só re-medir o + `grip_hold` na malha da arma nova (o cabo muda de lugar). +4. Re-medir só o que muda: `grip_hold` (centro do cabo na malha) e a escala (`CFG.len`). + Roll da mão e curl dos dedos **não mudam** por arma do mesmo tipo. + +### Outro PERSONAGEM +- Se o outro personagem usa **o mesmo rig Mixamo** (mesmos nomes de osso), os números da + seção 2 valem **iguais** — mão e dedos são função do rig, não do corpo. +- Se o rig diverge (proporção de braço/mão diferente), o **mount** e o **curl** valem, mas + os **alvos de IK** mudam (a mão nasce em outro lugar). Re-rodar o passo 3. +- Personagens de "braço-toco" (Dollynho, ET, Canarinho, Gotinha): a palma nasce dentro do + corpo — ver o tratamento que o jogo já faz (`torsoProfile`/`measurePalmLocal` em + `glbchars.js`) antes de assumir que a pega "bugou". + +--- + +## 4. Ligação com o sistema do jogo (não reinventar) + +O jogo **já resolve arma-na-mão** proceduralmente; esta pega manual é a versão "à mão" do +mesmo conceito. Pontos de conexão: + +- `weapons.js → gripPoints(id)`: `grip` (mão direita) e `fore` (mão esquerda no guarda-mão, + `null` p/ 1 mão). `ONE_HANDED = {pistol, deagle, revolver38, knife}`. +- `glbchars.js → buildCharacterModel`: monta a arma no `RightHand`, IK da mão de apoio + (`solveCCDIK`), curl dos dedos. **É o caminho da 3ª pessoa (bots).** +- `fparms.js → poseToWeapon`: braços dedicados de 1ª pessoa (`models/fparms/arms.glb`, + **sem ossos de dedo**), travados na arma por IK a cada frame. + +> **Implicação p/ 1ª pessoa:** o rig de braço FP (`arms.glb`) é diferente do Mixamo (não tem +> dedos). O que transfere direto é o **mount** (arma↔osso da mão) e a **orientação da pega**; +> o curl dos dedos não (dedos já vêm fechados na malha do `arms.glb`). + +--- + +## 5. Câmera 3ª pessoa no jogo (implementado) + +**Tecla `B`** alterna 1ª/3ª pessoa. Arquivo: `public/js/game.js`. + +Como funciona (reusa o que já existia): +- O corpo 3ª pessoa do jogador é o **mesmo `buildCharacterModel` dos bots** (`glbchars.js`) + — a arma monta na mão pelo grip procedural (mount no `RightHand` + IK da mão de apoio + + curl dos dedos). Nada de pose nova: é o mesmo caminho que já funciona pros bots. +- Métodos: `_toggleCamView` / `_ensurePlayerTP` (lazy build + upgrade box→GLB + rebuild ao + trocar de arma) / `_updatePlayerTP` (posição+yaw do jogador, locomoção pela velocidade, + crouch, mira da cabeça pelo pitch, câmera atrás). +- Visibilidade: em 3ª pessoa esconde `vm.root` (braços+arma FP) e mostra o corpo TP. +- Pré-carrega o modelo do personagem do jogador no construtor (a 1ª pessoa usa `arms.glb`, + não carrega o corpo), senão o TP cai no box. + +**Limitação declarada do V1:** mira/tiro continuam saindo da CÂMERA → em 3ª pessoa a origem +do disparo fica atrás do jogador. É um **modo de visão**, não de combate. Corrigir depois: +desacoplar a origem do tiro (olho) da câmera de render. + +**Como testar:** entrar numa partida, apertar **B**. Esperado: câmera vai pra trás e aparece +o mandrake segurando a pistola (o mesmo grip dos bots). Apertar B de novo volta pra 1ª pessoa. +Se o corpo aparecer virado de costas, é só o offset de yaw — trocar `p.yaw` por `p.yaw+Math.PI` +em `_updatePlayerTP`. + +## 6. Próximos + +- [x] Pega da pistola no mandrake (3ª pessoa, à mão) — números na seção 2, Action `pistol_aim_R`. +- [x] Toggle de **câmera 3ª pessoa** pro jogador (tecla B, reusa `buildCharacterModel`). +- [ ] Aplicar o mount à pistola na **1ª pessoa** (tunar `fparms.js`: offset/rot por arma). +- [ ] Tiro correto em 3ª pessoa (origem no olho, não na câmera). +- [ ] Replicar p/ rifle (2 mãos) e demais personagens. diff --git a/public/js/game.js b/public/js/game.js index 5280fdf6..4519af89 100644 --- a/public/js/game.js +++ b/public/js/game.js @@ -2,7 +2,7 @@ import * as THREE from 'three'; import { MAPS, resolveMapId } from './maps.js'; import { buildCharacter, poseCharacter, byId, CHARACTERS, buildRifle, charWeapon } from './characters.js'; -import { buildCharacterModel, hasModel } from './glbchars.js'; +import { buildCharacterModel, hasModel, preloadCharacterAssets } from './glbchars.js'; import { weaponModel, weaponCFG, ONE_HANDED, WEAPON_IDS, PISTOLS, gripPoints } from './weapons.js'; import { buildFPArms, poseToWeapon, FP_OFF } from './fparms.js'; import { VM_FRAME } from './vmattach.js'; @@ -679,6 +679,25 @@ export class Game { this.playerCharId = playerCharId; // usado por _buildViewModels (paleta/braços FP) e _resetPositions (loadout) this.combatants = []; // scoreboard entries + // ---- Câmera 3ª pessoa (toggle KeyB) ---------------------------------------- + // O jogo é 1ª pessoa; os bots já são 3ª pessoa (glbchars.js). Aqui o JOGADOR ganha + // um corpo 3ª pessoa (mesmo buildCharacterModel dos bots — a arma monta na mão pelo + // MESMO grip procedural: mount no RightHand + IK da mão de apoio + curl dos dedos) e + // uma câmera atrás dele. Tecla B alterna. Ver docs/RIG-PEGA-ARMA.md. + // V1 DECLARADO: mira/tiro continuam saindo da câmera — em 3ª pessoa a origem do + // disparo fica atrás do jogador. É um MODO DE VISÃO, não de combate. + // Pré-carrega o modelo do próprio personagem (a 1ª pessoa usa arms.glb, não carrega o + // corpo do jogador), senão o corpo TP cai no box procedural. + this.camView = 'first'; // 'first' (1ª pessoa) | 'third' (OTS SOCOM) | 'shoulder' (OTS colado) + this.playerTP = null; + this._tpWeapon = null; + this._tpDead = false; + this._tpFwd = new THREE.Vector3(); + this._tpRight = new THREE.Vector3(); + this._tpEul = new THREE.Euler(); + this._eyeWorld = new THREE.Vector3(); // posição do OLHO — origem de tiro/fumaça em 3ª pessoa + try { preloadCharacterAssets([playerCharId]); } catch {} + // ---- player ---- // Spawns holding the SAME weapon shown on the character-select screen (charWeapon). // primary/secondary remember the last weapon of each slot for the 1/2 keys. @@ -1602,6 +1621,8 @@ export class Game { } } // SÓ-ARMA: esconde as mãos procedurais (handR/handL) presas a cada modelo de arma. + // (A handR procedural é cápsulas calibradas pra arma-box antiga — na arma Mint cai + // torta. Mão boa vem do corpo real do personagem: ver os modos de 3ª pessoa (camView).) if (WEAPON_ONLY) for (const k in models) models[k].traverse((o) => { if (o.name === 'handR' || o.name === 'handL') o.visible = false; }); this._weaponOnly = WEAPON_ONLY; // grip/ads expostos no objeto do VM (G3-R1): `grip[id]` é o PONTO DE EMPUNHADURA em @@ -1811,6 +1832,7 @@ export class Game { if (e.code === 'KeyZ') { this._radioShow('z'); return; } if (e.code === 'KeyX') { this._radioShow('x'); return; } if (e.code === 'KeyV') { this._radioShow('c'); return; } + if (e.code === 'KeyB') { this._toggleCamView(); return; } // 1ª/3ª pessoa on/off // slot memory: 1 = last primary held, 2 = last sidearm held (not a hardcoded reset) if (e.code === 'Digit1') this._switchWeapon(this.player.primary || 'awp'); if (e.code === 'Digit2') this._switchWeapon(this.player.secondary || 'pistol'); @@ -2991,7 +3013,7 @@ export class Game { const spreadBase = (GUNFEEL ? (w.spreadHip + (spScoped - w.spreadHip) * adsF) : (p.weapon === 'awp' ? (p.scoped ? w.spreadScope : w.spreadHip) : w.spreadHip)) * crouchMul * moveMul; - const from = this.camera.getWorldPosition(new THREE.Vector3()); + const from = this._aimOrigin(new THREE.Vector3()); const pellets = w.pellets || 1; // tracer só em PARTE dos tiros (CS): 1 em 3 na rajada; sniper/shotgun sempre (o tiro é o // evento). Antes TODO tiro deixava rastro — vira "chuva de laser" em full-auto. @@ -3038,7 +3060,7 @@ export class Game { if (p.scoped && (p.weapon === 'awp' || p.weapon === 'mosin' || p.weapon === 'rem700')) this._scope(false, true); } _meleeHit() { - const from = this.camera.getWorldPosition(new THREE.Vector3()); + const from = this._aimOrigin(new THREE.Vector3()); const dir = new THREE.Vector3(0, 0, -1).applyQuaternion(this.camera.quaternion); let best = null, bd = WEAPONS.knife.range; for (const b of this.bots) { @@ -3674,12 +3696,27 @@ export class Game { // pelo matrixWorld ATUAL do vm.root (com o kick acumulado) e depois pela câmera — usado // pelo tracer e pela luz/faísca do mundo no tiro do jogador (R7.6). _muzzleWorld(cls) { + if (this.camView !== 'first') { + // 3ª pessoa: a câmera está atrás/acima; a boca do cano NÃO pode sair dela (fumaça + // nascia lá em cima — bug relatado). Sai do OLHO, na direção da mira, descida pra + // altura da arma na mão. + const dir = this.camera.getWorldDirection(new THREE.Vector3()); + const m = this._eyeWorld.clone().addScaledVector(dir, 0.5); + m.y -= 0.28; + return m; + } const off = this._vmMuzzle[this.player?.weapon] || this._vmMuzzle[cls] || this._vmMuzzle.rifle; this.vm.root.updateWorldMatrix(true, false); const v = off.clone(); this.vm.root.localToWorld(v); // vmScene == espaço da câmera (vmCamera na origem) return this.camera.localToWorld(v); } + + // Origem do tiro/raycast: olho do jogador. Em 3ª pessoa a câmera está atrás — usar a + // câmera faria a bala nascer nas costas. Ver _eyeWorld (capturado no _updatePlayer). + _aimOrigin(out) { + return this.camView === 'first' ? this.camera.getWorldPosition(out) : out.copy(this._eyeWorld); + } // Porta com SENSOR (Havan): desliza as 2 folhas ao chegar perto (player ou bot). Painéis são // só visuais (não colidem), então quando você alcança a porta já está aberta. _updateDoors(dt) { @@ -4816,6 +4853,105 @@ export class Game { if (this.vm?.root) this.vm.root.visible = false; if (this.el.crosshair) this.el.crosshair.style.display = 'none'; } + // Cicla 1ª → 3ª (longe) → ombro (perto, vê braços) → 1ª (tecla B). Constrói o corpo + // 3ª pessoa na 1ª vez (lazy). + _toggleCamView() { + // first = 1ª pessoa (só-arma, padrão do jogo) · third = OTS SOCOM (cintura pra cima) · + // shoulder = OTS colado (vê a arma de perto). A 1ª-pessoa-com-corpo (fpbody) foi removida. + const order = ['first', 'third', 'shoulder']; + this.camView = order[(order.indexOf(this.camView) + 1) % order.length]; + if (this.camView !== 'first') this._ensurePlayerTP(); + this._syncCamViewVis(); + } + + // Visibilidade base ao alternar: corpo TP em qualquer modo 3ª pessoa. (Os braços FP/ + // vm.root são governados por frame no _updatePlayer — ver a linha do realScope — pra não + // brigar com o force-visible do ADS/luneta.) + _syncCamViewVis() { + const tp = this.camView !== 'first'; + if (this.playerTP && this.playerTP.group) this.playerTP.group.visible = tp; + if (this.vm && this.vm.root && !tp) this.vm.root.visible = true; + } + + // (Re)constrói o corpo 3ª pessoa do jogador com a arma ATUAL montada na mão. Reusa + // buildCharacterModel (mesmo caminho dos bots: grip + IK + curl). Se o modelo do + // personagem ainda não carregou, cai no buildCharacter (box) e faz upgrade p/ GLB + // quando o asset chegar (hasModel). Reconstrói ao trocar de arma. + _ensurePlayerTP() { + const w = this.player.weapon, def = this.playerDef; + const weaponChanged = this.playerTP && this._tpWeapon !== w; + const wantUpgrade = this.playerTP && !this.playerTP.isGLB && hasModel(def.id); + if (this.playerTP && !weaponChanged && !wantUpgrade) return; + // Tenta o GLB (grip real). Se ainda não carregou e já temos algo, mantém o atual — + // evita reconstruir o box a cada frame enquanto os clips não chegam. + const glb = buildCharacterModel(def, { weaponId: w }); + if (!glb && this.playerTP && !weaponChanged) return; + if (this.playerTP && this.playerTP.group) { + this.scene.remove(this.playerTP.group); + if (!this.playerTP.isGLB) this.playerTP.group.traverse(o => { if (o.geometry) o.geometry.dispose(); }); + } + const tp = glb || buildCharacter(def); + // O corpo do próprio jogador nunca é alvo/obstáculo de raycast (o hitscan mira só os + // grupos dos bots, mas marcamos por garantia — mesmo contrato do gun mount). + tp.group.traverse(o => { o.userData.noHit = true; o.userData.selfBody = true; }); + tp.group.visible = (this.camView === 'third'); + this.scene.add(tp.group); + this.playerTP = tp; + this._tpWeapon = w; + } + + // Anima o corpo TP a partir do estado do jogador e põe a câmera atrás dele. + // `sp` = velocidade horizontal real; `eye` = altura do olho já com crouch/dip. + _updatePlayerTP(dt, sp, eye) { + this._ensurePlayerTP(); + const tp = this.playerTP, p = this.player; + if (!tp) return; + // corpo: posição + yaw do jogador (mesma convenção dos bots: rotation.y = yaw). + tp.group.visible = true; + tp.group.position.copy(p.pos); + tp.group.rotation.set(0, p.yaw + Math.PI, 0); // +180°: o corpo olha pra onde a câmera olha (de costas p/ ela) + // animação: locomoção pela velocidade + agachado + mira da cabeça pelo pitch. + if (tp.ctrl) { + tp.ctrl.setCrouch?.(p.crouchF > 0.5); + tp.ctrl.aimPitch = p.pitch; + tp.ctrl.update(dt, sp > 0.6 ? 1 : 0, false, sp, false); + } else if (tp.mixer) { + tp.mixer.update(dt); + } + this._tpEul.set(p.pitch, p.yaw, 0, 'YXZ'); + const fwd = this._tpFwd.set(0, 0, -1).applyEuler(this._tpEul); + const right = this._tpRight.set(1, 0, 0).applyEuler(this._tpEul); + const cam = this.camera; + // 'third' = over-the-shoulder perto, cintura pra cima (estilo SOCOM): ~1,7 m atrás, + // ~1,8 m de altura olhando reto → pernas caem fora. 'shoulder' = mais colado ainda. + const shoulder = this.camView === 'shoulder'; + const TP_DIST = shoulder ? 0.85 : 1.7; + const TP_UP = shoulder ? 0.10 : 0.18; + const TP_SIDE = shoulder ? 0.34 : 0.28; + cam.position.set(p.pos.x, p.pos.y + eye, p.pos.z).addScaledVector(fwd, -TP_DIST).addScaledVector(right, TP_SIDE); + cam.position.y += TP_UP; + const gy = this.world.groundHeightAt(cam.position.x, cam.position.z, cam.position.y) + 0.2; + if (cam.position.y < gy) cam.position.y = gy; // não atravessa o chão + cam.rotation.set(p.pitch, p.yaw, 0); + } + + // Morte em 3ª pessoa (e também visível na 1ª, na câmera de morte): toca a animação de + // queda no corpo TP e o mantém caindo. Sem isto o boneco ficava de pé (bug relatado). + _tpDeath(dt) { + this._ensurePlayerTP(); + const tp = this.playerTP, p = this.player; + if (!tp) return; + if (!this._tpDead) { + this._tpDead = true; + tp.group.visible = true; // mostra o corpo caindo mesmo em 1ª pessoa + tp.group.position.copy(p.pos); + tp.group.rotation.set(0, p.yaw + Math.PI, 0); + if (tp.ctrl && tp.ctrl.die) tp.ctrl.die(); + } + if (tp.ctrl) tp.ctrl.update(dt, 0, false, 0, false); + else if (tp.mixer) tp.mixer.update(dt); + } + _updatePlayer(dt) { const p = this.player; this._checkCtfAlvo(); // alvo de BANDEIRAS: única condição de vitória da rodada de CAPTURA (sem gate) @@ -4825,11 +4961,18 @@ export class Game { const left = p.respawnAt - this.time; this.el.respawnCount.textContent = Math.max(0, left).toFixed(1); this._deathFeedback(dt); + this._tpDeath(dt); // corpo TP cai (item: "boneco não caía morto") if (left <= 0) this._respawnPlayer(); this.camera.position.y = Math.max(0.5, this.camera.position.y - dt * 2); this.camera.rotation.z = Math.min(0.5, (this.camera.rotation.z || 0) + dt * 0.8); return; } + // ressuscitou: desfaz a morte do corpo TP e ajusta visibilidade ao modo atual. + if (this._tpDead) { + this._tpDead = false; + if (this.playerTP && this.playerTP.ctrl && this.playerTP.ctrl.revive) this.playerTP.ctrl.revive(); + if (this.playerTP && this.playerTP.group) this.playerTP.group.visible = this.camView !== 'first'; + } if (this.mobile && this.state === 'live') this._aimAssist(dt); // sticky aim (a mira é por arraste) // REGEN fora de combate (ver comentário da constante). Detecta o dano pela QUEDA do hp — // o _damage fica fora desta região de edição, então não dá pra marcar o timestamp lá. @@ -4954,6 +5097,9 @@ export class Game { // → lê como "tremida" e não como punch puro. Vale pra mouse E toque (câmera finaliza só aqui). const roll = p._rec ? p._rec.sh * Math.sin(this.time * 61) * 0.5 : 0; this.camera.rotation.set(p.pitch + rp, p.yaw, roll); + this._eyeWorld.copy(this.camera.position); // olho: origem de tiro/fumaça em 3ª pessoa + // 3ª pessoa (longe/ombro): sobrepõe a câmera e anima o corpo TP. No-op na 1ª pessoa. + if (this.camView !== 'first') this._updatePlayerTP(dt, sp, eye); // footsteps + view bob const moving = sp > 0.6 && p.grounded; if (moving) { @@ -5006,7 +5152,8 @@ export class Game { // dynamic crosshair gap (movement/spray opens it, crouch + ADS tighten it) const gap = precAds ? 3 : Math.max(3, Math.min(26, 5 + sp * 1.15 + this.vm.kick * 20 - p.crouchF * 2.5 - (p.scoped ? 4 : 0))); this.el.crosshair.style.setProperty('--ch', gap.toFixed(1) + 'px'); - this.vm.root.visible = !(realScope && mask > 0.55); // a arma só sai de cena depois que a luneta cobre + // 3ª pessoa esconde os braços/arma FP (o corpo TP tem a própria arma na mão). + this.vm.root.visible = this.camView === 'first' && !(realScope && mask > 0.55); // a arma só sai de cena depois que a luneta cobre // reload completion — RELÓGIO DE JOGO (devolve a munição). A ANIMAÇÃO é do rig e usa a // mesma duração da tabela, então as duas pontas chegam no mesmo quadro (BUG-04). if (!this._reloading() && p.reloadUntil > 0) { From 7535712a19133b313f49d8142f49bc7a1fea62ec Mon Sep 17 00:00:00 2001 From: csbrasil-bot Date: Mon, 24 Aug 2026 12:37:31 +0000 Subject: [PATCH 16/16] chore(docs): regenera bloco derivado (autofix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rodado pelo autofix: só arquivo gerado, conferido pela lista de permissão. Signed-off-by: csbrasil-bot --- ARCH.generated.md | 2 +- README.md | 4 ++-- STATUS.md | 2 +- docs/docs/arquitetura.md | 4 ++-- docs/docs/comecando.md | 4 ++-- .../en/docusaurus-plugin-content-docs/current/arquitetura.md | 4 ++-- .../en/docusaurus-plugin-content-docs/current/comecando.md | 4 ++-- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ARCH.generated.md b/ARCH.generated.md index 3fc30413..8333cccd 100644 --- a/ARCH.generated.md +++ b/ARCH.generated.md @@ -8,7 +8,7 @@ Números atuais das zonas, do quality gate e do `package.json`: | Zona | O que é | Tamanho medido | Regra | |---|---|---|---| -| `public/` | o **jogo** | 44 arquivos `.js`, 32.027 linhas · Three.js `r160` vendorizado | ES modules servidos crus, **zero build**, sem dependência de runtime | +| `public/` | o **jogo** | 44 arquivos `.js`, 32.174 linhas · Three.js `r160` vendorizado | ES modules servidos crus, **zero build**, sem dependência de runtime | | `src/` | o **site** | 18 páginas `.astro`, 21 rotas `/api` · Astro `^7.1.1` | framework é bem-vindo; `service_role` só no servidor | | `tools/` | o **arnês** | 201 scripts em `tools/eval/`, 55 em `tools/` | node puro: sobe o jogo real sem browser | diff --git a/README.md b/README.md index 23174d37..4f7434eb 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 32.027 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | -| `game.js` | **6.936** linhas | `wc -l public/js/game.js` | +| Código do jogo | 32.174 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **7.083** linhas | `wc -l public/js/game.js` | | `main.js` | 2.698 linhas | `wc -l public/js/main.js` | | Armas com GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | | GLBs de personagem | 45 | `git ls-files 'public/models/characters/*.glb' \| wc -l` | diff --git a/STATUS.md b/STATUS.md index 915d188c..0fda9433 100644 --- a/STATUS.md +++ b/STATUS.md @@ -4,7 +4,7 @@ - **Versão:** `2.0.0-alpha.183` - **Conteúdo jogável:** 5 facções, 44 personagens, 12 mapas e 26 armas com GLB -- **Código do jogo:** 32.027 linhas em 44 módulos JavaScript +- **Código do jogo:** 32.174 linhas em 44 módulos JavaScript - **Automação:** 124 comandos npm, 201 scripts de avaliação e 55 scripts de pipeline > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `package.json · CHARACTERS · MAPS · public/models/weapons · public/js · tools/` diff --git a/docs/docs/arquitetura.md b/docs/docs/arquitetura.md index 803f9da6..60aca2be 100644 --- a/docs/docs/arquitetura.md +++ b/docs/docs/arquitetura.md @@ -62,7 +62,7 @@ Tamanho dos arquivos que o `gen-arch.mjs` indexa — bloco gerado, regenerado po | Arquivo | Linhas | |---|---:| -| `public/js/game.js` | 6.936 | +| `public/js/game.js` | 7.083 | | `public/js/main.js` | 2.698 | | `public/js/characters.js` | 1.068 | | `public/js/glbchars.js` | 844 | @@ -70,7 +70,7 @@ Tamanho dos arquivos que o `gen-arch.mjs` indexa — bloco gerado, regenerado po | `public/js/weapons.js` | 346 | | `public/js/springs.js` | 260 | -Total de `public/js/`: **32.027 linhas em 44 arquivos**. O índice símbolo→linha, com a tabela de conflito, é outro bloco gerado: `tools/eval/ARCH.md` (`npm run arch`). +Total de `public/js/`: **32.174 linhas em 44 arquivos**. O índice símbolo→linha, com a tabela de conflito, é outro bloco gerado: `tools/eval/ARCH.md` (`npm run arch`). > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: ``git ls-files public/js/*.js | xargs wc -l`` diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index 76cc679a..11091304 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -39,8 +39,8 @@ esta página envelhecia no primeiro commit — ver | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 32.027 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | -| `game.js` | **6.936** linhas | `wc -l public/js/game.js` | +| Código do jogo | 32.174 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **7.083** linhas | `wc -l public/js/game.js` | | `main.js` | 2.698 linhas | `wc -l public/js/main.js` | | Armas com GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | | GLBs de personagem | 45 | `git ls-files 'public/models/characters/*.glb' \| wc -l` | diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md index 302c92fd..383df90d 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md @@ -69,7 +69,7 @@ Size of the files `gen-arch.mjs` indexes — a generated block, regenerated by | File | Lines | |---|---:| -| `public/js/game.js` | 6,936 | +| `public/js/game.js` | 7,083 | | `public/js/main.js` | 2,698 | | `public/js/characters.js` | 1,068 | | `public/js/glbchars.js` | 844 | @@ -77,7 +77,7 @@ Size of the files `gen-arch.mjs` indexes — a generated block, regenerated by | `public/js/weapons.js` | 346 | | `public/js/springs.js` | 260 | -Total in `public/js/`: **32,027 lines in 44 files**. The symbol-to-line index lives in `tools/eval/ARCH.md`. +Total in `public/js/`: **32,174 lines in 44 files**. The symbol-to-line index lives in `tools/eval/ARCH.md`. > Block generated by `node tools/gen-docs.mjs`. Source: ``wc -l public/js/*.js`` diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md index f17282d4..03054be3 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md @@ -41,8 +41,8 @@ this page was aging at the very first commit — see | What | How much | Where to check | |---|---:|---| -| Game code | 32,027 lines in 44 files | `git ls-files public/js/*.js \| xargs wc -l` | -| `game.js` | **6,936** lines | `wc -l public/js/game.js` | +| Game code | 32,174 lines in 44 files | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **7,083** 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` |