diff --git a/ARCH.generated.md b/ARCH.generated.md index 725b84f0c..c1441775e 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`, 32.001 linhas · Three.js `r160` vendorizado | ES modules servidos crus, **zero build**, sem dependência de runtime | +| `public/` | o **jogo** | 45 arquivos `.js`, 32.271 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 | +| `tools/` | o **arnês** | 202 scripts em `tools/eval/`, 55 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. diff --git a/KNOWN-BUGS.md b/KNOWN-BUGS.md index e9db99203..50cc58a91 100644 --- a/KNOWN-BUGS.md +++ b/KNOWN-BUGS.md @@ -2970,6 +2970,25 @@ publicação em potencial, e o `.gitignore` não protege de um deploy local. `signal=`/`status=`. Prova operacional: depois do restart, nenhuma linha `poll:` nova e `estado.main` avança commit a commit. Contrato novo junto: achado grave vira `REQUEST_CHANGES` e fio não resolvido trava o merge via branch protection "require resolved conversations" (antes: "comenta, não bloqueia"). +- **BUG-68 · RESOLVIDO — rampa do Treta no Gelo aparecia sem textura.** Palavras do dono: + *"A rampa esta sem textura"*. A régua existente só conferia `material.map.name`, portanto + ficava verde mesmo com UV degenerado nas faces laterais da cunha. **Evidência:** GELO8 em + `eval:gelo` media 67%/67% de cobertura UV antes e 100%/100% depois de separar as costuras + das faces. O mutante `rampa-sem-uv` reprova em 0%/0%. A captura real confirmou os blocos + de gelo contínuos nas faces inclinadas e laterais. **Custo:** 18 vértices adicionais por + rampa, sem textura, material ou draw call extra. + +- **~~BUG-67 · rampas do Treta no Gelo não são transitáveis e destoam dos blocos~~ · RESOLVIDO 18/08.** Palavras do + dono: *"Deixe as rampas com a cor dos blocos e faca com que seja possivel subir nela"*. + A rampa visível foi cadastrada como um AABB sólido de 2,4 m e o mapa exporta + `groundHeightAt: () => 0`; portanto o motor enxergava parede, não inclinação. Agora a + altura acompanha continuamente a cunha, o AABB foi retirado e a geometria tem UV para + receber a mesma `gelo-pedra` da fortaleza. **Antes:** GELO6 vermelha. **Depois:** o + `_updatePlayer()` real subiu ambas as rampas até 2,39 m. A cláusula GELO6 de `eval:gelo` + mede altura, colisão, material e movimento; `rampa-plana`, `rampa-parede` e `rampa-cor` + deixam a cláusula vermelha. **Custo:** 27 → 25 colisores; as rampas deixam de ocluir por + AABB, mas continuam em `occluders` para linha de visão e o grafo permanece conectado. + - **~~BUG-66 · Faria Limer ainda fala com a voz do Lula~~ · RESOLVIDO 16/08.** Palavras do dono: *"o farialimer ainda tá com som do Lula; precisamos usar um do time do Bolsonaro"*. O vínculo explícito criado no BUG-65 aponta para `55678d5886537476`, hash do arquivo-fonte `cana_doce.mp3`: diff --git a/README.md b/README.md index cc453b6d2..d63c8f1ec 100644 --- a/README.md +++ b/README.md @@ -34,17 +34,17 @@ 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` | -| `main.js` | 2.698 linhas | `wc -l public/js/main.js` | +| Código do jogo | 32.271 linhas em 45 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **6.917** linhas | `wc -l public/js/game.js` | +| `main.js` | 2.702 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` | | Props em GLB | 108 | `git ls-files 'public/models/props/*.glb' \| wc -l` | | Clipes de animação versionados | 573 | `git ls-files public/models/anims \| wc -l` | | Personagens jogáveis | 44, em 5 facções | array `CHARACTERS` de `characters.js` | -| Mapas no registro | 12 | objeto `MAPS` de `maps.js` | +| Mapas no registro | 13 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 201 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 202 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 55 | `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.186` | `public/js/version.js` e `package.json` (batem) | @@ -89,7 +89,7 @@ arquitetura): `cd docs && npm install && npm start` → Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `objeto MAPS de public/js/maps.js` diff --git a/STATUS.md b/STATUS.md index 02b49e34b..c021663ae 100644 --- a/STATUS.md +++ b/STATUS.md @@ -3,9 +3,9 @@ - **Versão:** `2.0.0-alpha.186` -- **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 -- **Automação:** 124 comandos npm, 201 scripts de avaliação e 55 scripts de pipeline +- **Conteúdo jogável:** 5 facções, 44 personagens, 13 mapas e 26 armas com GLB +- **Código do jogo:** 32.271 linhas em 45 módulos JavaScript +- **Automação:** 124 comandos npm, 202 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 be5416328..a24b8c03e 100644 --- a/docs/docs/arquitetura.md +++ b/docs/docs/arquitetura.md @@ -62,15 +62,15 @@ Tamanho dos arquivos que o `gen-arch.mjs` indexa — bloco gerado, regenerado po | Arquivo | Linhas | |---|---:| -| `public/js/game.js` | 6.910 | -| `public/js/main.js` | 2.698 | +| `public/js/game.js` | 6.917 | +| `public/js/main.js` | 2.702 | | `public/js/characters.js` | 1.068 | | `public/js/glbchars.js` | 844 | | `public/js/vmattach.js` | 628 | | `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.271 linhas em 45 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/colaborar.md b/docs/docs/colaborar.md index 2d5b3e074..10057223a 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**. +**12 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`, `usantos`. 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)` @@ -260,8 +260,9 @@ O registro, gerado do `MAPS` de `public/js/maps.js`: | `parque_treta` | Parque da Treta | **captura** | `map_parque.js` | 402 | | `velho_oeste` | Velho Oeste da Treta | **captura** | `map_velho_oeste.js` | 433 | | `penitenciaria` | Penitenciária da Treta | **captura** | `map_penitenciaria.js` | 247 | +| `treta_no_gelo` | Treta no Gelo | rodadas | `map_gelo.js` | 250 | -**12 mapas registrados** — 2 abrem em rodadas e 10 em captura. `ctfMode` **abre** o mapa em captura, não prende: o jogador troca no menu (é a `MOD1`). Há 14 arquivos `map_*.js` em `public/js/` — arquivo no disco **não** implica mapa jogável. +**13 mapas registrados** — 3 abrem em rodadas e 10 em captura. `ctfMode` **abre** o mapa em captura, não prende: o jogador troca no menu (é a `MOD1`). Há 15 arquivos `map_*.js` em `public/js/` — arquivo no disco **não** implica mapa jogável. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `objeto MAPS de public/js/maps.js` diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index fca2929b5..4052d046a 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -39,17 +39,17 @@ 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` | -| `main.js` | 2.698 linhas | `wc -l public/js/main.js` | +| Código do jogo | 32.271 linhas em 45 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **6.917** linhas | `wc -l public/js/game.js` | +| `main.js` | 2.702 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` | | Props em GLB | 108 | `git ls-files 'public/models/props/*.glb' \| wc -l` | | Clipes de animação versionados | 573 | `git ls-files public/models/anims \| wc -l` | | Personagens jogáveis | 44, em 5 facções | array `CHARACTERS` de `characters.js` | -| Mapas no registro | 12 | objeto `MAPS` de `maps.js` | +| Mapas no registro | 13 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 201 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 202 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 55 | `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.186` | `public/js/version.js` e `package.json` (batem) | @@ -66,7 +66,7 @@ E as regras de partida que mais mudam de lugar, todas lidas das constantes de | Regra | Valor | Constante | |---|---|---| | Facções · personagens | 5 · 44 (B 9 · C 9 · E 8 · F 9 · U 9) | `CHARACTERS` | -| Mapas no menu | 12 — 2 abrem em rodadas, **10 em captura** | `MAPS` / `ctfMode` | +| Mapas no menu | 13 — 3 abrem em rodadas, **10 em captura** | `MAPS` / `ctfMode` | | Respawn | 2,2 s | `RESPAWN_DELAY` | | Round | 99 s, 3 vitórias | `ROUND_TIME` / `ROUNDS_TO_WIN` | | Captura | alvo = **todas as bandeiras do mapa**, 2 rodadas (rede de segurança 480 s) | `capsToWin = ctfPts.length` / `CTF_ROUNDS_TO_WIN` | @@ -233,8 +233,9 @@ Os mapas registrados hoje, e em que modo cada um abre: | `parque_treta` | Parque da Treta | **captura** | `map_parque.js` | 402 | | `velho_oeste` | Velho Oeste da Treta | **captura** | `map_velho_oeste.js` | 433 | | `penitenciaria` | Penitenciária da Treta | **captura** | `map_penitenciaria.js` | 247 | +| `treta_no_gelo` | Treta no Gelo | rodadas | `map_gelo.js` | 250 | -**12 mapas registrados** — 2 abrem em rodadas e 10 em captura. `ctfMode` **abre** o mapa em captura, não prende: o jogador troca no menu (é a `MOD1`). Há 14 arquivos `map_*.js` em `public/js/` — arquivo no disco **não** implica mapa jogável. +**13 mapas registrados** — 3 abrem em rodadas e 10 em captura. `ctfMode` **abre** o mapa em captura, não prende: o jogador troca no menu (é a `MOD1`). Há 15 arquivos `map_*.js` em `public/js/` — arquivo no disco **não** implica mapa jogável. > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `objeto MAPS de public/js/maps.js` diff --git a/docs/docs/quality-gates.md b/docs/docs/quality-gates.md index d802c0845..e76544e9d 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 **201 scripts** em `tools/eval/` (`.mjs` + `.py`), mais **55 scripts** de pipeline em `tools/`. +- O arnês inteiro são **202 scripts** em `tools/eval/` (`.mjs` + `.py`), mais **55 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 e5818daeb..0d039613f 100644 --- a/docs/docs/stack.md +++ b/docs/docs/stack.md @@ -17,7 +17,7 @@ a partir do `package.json`, do `docs/package.json` e do próprio Three.js vendor | Camada | Ferramenta | Versão | |---|---|---| | Motor 3D (WebGL) | **Three.js**, vendorizado | `r160` | -| Jogo | ES modules vanilla, **zero build** | 44 arquivos | +| Jogo | ES modules vanilla, **zero build** | 45 arquivos | | Site | **Astro** com SSR | `^7.1.1` | | Hospedagem | adapter **Vercel** | `^11.0.6` | | Banco | **Postgres gerenciado** (RLS; schema privado, fora do repo) | `^2.110.7` | 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 bf263a227..0fbd66bb3 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md @@ -69,15 +69,15 @@ Size of the files `gen-arch.mjs` indexes — a generated block, regenerated by | File | Lines | |---|---:| -| `public/js/game.js` | 6,910 | -| `public/js/main.js` | 2,698 | +| `public/js/game.js` | 6,917 | +| `public/js/main.js` | 2,702 | | `public/js/characters.js` | 1,068 | | `public/js/glbchars.js` | 844 | | `public/js/vmattach.js` | 628 | | `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,271 lines in 45 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/colaborar.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/colaborar.md index ad72a3a1d..25c4ae7e8 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. +**12 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`, `usantos`. 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)` @@ -263,8 +263,9 @@ The registry, generated from the `MAPS` of `public/js/maps.js`: | `parque_treta` | Parque da Treta | **capture** | `map_parque.js` | 402 | | `velho_oeste` | Velho Oeste da Treta | **capture** | `map_velho_oeste.js` | 433 | | `penitenciaria` | Penitenciária da Treta | **capture** | `map_penitenciaria.js` | 247 | +| `treta_no_gelo` | Treta no Gelo | rounds | `map_gelo.js` | 250 | -**12 registered maps** - 2 open in rounds and 10 in capture. `ctfMode` sets the initial mode; it does not lock it. There are 14 `map_*.js` files on disk, so a file alone does **not** make a map playable. +**13 registered maps** - 3 open in rounds and 10 in capture. `ctfMode` sets the initial mode; it does not lock it. There are 15 `map_*.js` files on disk, so a file alone does **not** make a map playable. > Block generated by `node tools/gen-docs.mjs`. Source: `objeto MAPS de public/js/maps.js` 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 71fc2e8c7..fae190d44 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md @@ -41,17 +41,17 @@ 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` | -| `main.js` | 2,698 lines | `wc -l public/js/main.js` | +| Game code | 32,271 lines in 45 files | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **6,917** lines | `wc -l public/js/game.js` | +| `main.js` | 2,702 lines | `wc -l public/js/main.js` | | Weapons with GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | | Character GLBs | 45 | `git ls-files 'public/models/characters/*.glb' \| wc -l` | | Props in GLB | 108 | `git ls-files 'public/models/props/*.glb' \| wc -l` | | Versioned animation clips | 573 | `git ls-files public/models/anims \| wc -l` | | Playable characters | 44, in 5 factions | `CHARACTERS` array in `characters.js` | -| Maps in the registry | 12 | `MAPS` object in `maps.js` | +| Maps in the registry | 13 | `MAPS` object in `maps.js` | | Visual harnesses in HTML | 15 | `git ls-files 'public/*.html' \| wc -l` | -| Harness scripts | 201 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Harness scripts | 202 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Pipeline scripts | 55 | `git ls-files 'tools/*.mjs' \| wc -l` | | Written entry tasks | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | | Version | `2.0.0-alpha.186` | `public/js/version.js` and `package.json` (match) | @@ -68,7 +68,7 @@ And the match rules that move around the most, all read from the constants in | Rule | Value | Constant | |---|---|---| | Factions · characters | 5 · 44 (B 9 · C 9 · E 8 · F 9 · U 9) | `CHARACTERS` | -| Maps in the menu | 12 - 2 open in rounds, **10 in capture** | `MAPS` / `ctfMode` | +| Maps in the menu | 13 - 3 open in rounds, **10 in capture** | `MAPS` / `ctfMode` | | Respawn | 2.2 s | `RESPAWN_DELAY` | | Round | 99 s, 3 wins | `ROUND_TIME` / `ROUNDS_TO_WIN` | | Capture | target = **all flags on the map**, 2 rounds (480 s safety net) | `capsToWin = ctfPts.length` / `CTF_ROUNDS_TO_WIN` | @@ -235,8 +235,9 @@ The maps registered today, and which mode each one opens in: | `parque_treta` | Parque da Treta | **capture** | `map_parque.js` | 402 | | `velho_oeste` | Velho Oeste da Treta | **capture** | `map_velho_oeste.js` | 433 | | `penitenciaria` | Penitenciária da Treta | **capture** | `map_penitenciaria.js` | 247 | +| `treta_no_gelo` | Treta no Gelo | rounds | `map_gelo.js` | 250 | -**12 registered maps** - 2 open in rounds and 10 in capture. `ctfMode` sets the initial mode; it does not lock it. There are 14 `map_*.js` files on disk, so a file alone does **not** make a map playable. +**13 registered maps** - 3 open in rounds and 10 in capture. `ctfMode` sets the initial mode; it does not lock it. There are 15 `map_*.js` files on disk, so a file alone does **not** make a map playable. > Block generated by `node tools/gen-docs.mjs`. Source: `objeto MAPS de public/js/maps.js` 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 2a6ea015a..b56f29e01 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 **201 scripts** in `tools/eval/`, plus **55 pipeline scripts** in `tools/`. +- The harness contains **202 scripts** in `tools/eval/`, plus **55 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 fa6e23626..0c5a0220a 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/stack.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/stack.md @@ -20,7 +20,7 @@ from `package.json`, `docs/package.json` and the vendored Three.js itself. | Layer | Tool | Version | |---|---|---| | 3D engine (WebGL) | **Three.js**, vendored | `r160` | -| Game | vanilla ES modules, **zero build** | 44 files | +| Game | vanilla ES modules, **zero build** | 45 files | | Site | **Astro** with SSR | `^7.1.1` | | Hosting | **Vercel** adapter | `^11.0.6` | | Database | **managed Postgres** (RLS; private schema) | `^2.110.7` | diff --git a/public/img/map-previews/treta_no_gelo.jpg b/public/img/map-previews/treta_no_gelo.jpg new file mode 100644 index 000000000..c9dc9fc0a Binary files /dev/null and b/public/img/map-previews/treta_no_gelo.jpg differ diff --git a/public/js/audio.js b/public/js/audio.js index a378566c8..ccfa5e120 100644 --- a/public/js/audio.js +++ b/public/js/audio.js @@ -528,8 +528,15 @@ export class Sfx { land() { this.ensure(); this._burst(.08, .18, 400); } // passos: round-robin nos samples (nunca repete o anterior) + playbackRate ±8% + // volume ±15%. Fallback synth: timbre por surface (água do piscinão = splash grave+ruído, - // metal = brilho agudo, concreto = seco). surface vem do game.js (world.slowAt). + // gelo = crocância seca + cristais agudos, metal = brilho agudo, concreto = seco). step(surface = 'concrete') { + if (surface === 'ice') { + this.ensure(); + const v = 0.85 + Math.random() * 0.3; + this._burst(.055, .075 * v, 620 + Math.random() * 260, .75, 'lowpass'); + this._burst(.075, .045 * v, 2600 + Math.random() * 1100, 1.8, 'bandpass', .008); + return; + } const arr = this.pack?.cs?.footsteps; if (arr && arr.length) { this._stepI = (this._stepI + 1) % arr.length; diff --git a/public/js/game.js b/public/js/game.js index c9d32beeb..7bfc30c2f 100644 --- a/public/js/game.js +++ b/public/js/game.js @@ -4466,7 +4466,8 @@ export class Game { while (dy > Math.PI) dy -= Math.PI * 2; while (dy < -Math.PI) dy += Math.PI * 2; const cturn = dy * Math.min(1, dt * 8); b.yaw += BOT_MOVE2 ? Math.max(-YAW_CAP * dt, Math.min(YAW_CAP * dt, cturn)) : cturn; - const bSlow = this.world.slowAt && this.world.slowAt(b.pos.x, b.pos.z) ? 0.5 : 1; + const bSlow = this.world.speedMulAt?.(b.pos.x, b.pos.z) + ?? (this.world.slowAt && this.world.slowAt(b.pos.x, b.pos.z) ? 0.5 : 1); const px = b.pos.x, pz = b.pos.z; b.pos.x += Math.sin(b.yaw) * BOT_SPEED * bSlow * dt; b.pos.z += Math.cos(b.yaw) * BOT_SPEED * bSlow * dt; @@ -4823,7 +4824,8 @@ export class Game { p.crouchF = Math.max(0, Math.min(1, p.crouchF + (wantCrouch ? dt * 7 : -dt * (MOVE2 ? 4.2 : 7)))); const walking = MOVE2 && !!(this.keys.ShiftLeft || this.keys.ShiftRight); // Shift = ANDAR (silencioso) const sprint = !MOVE2 && !!(this.keys.ShiftLeft || this.keys.ShiftRight) && p.crouchF < 0.3; - const slowMul = this.world.slowAt && this.world.slowAt(p.pos.x, p.pos.z) ? 0.45 : 1; // água/lago + const slowMul = this.world.speedMulAt?.(p.pos.x, p.pos.z) + ?? (this.world.slowAt && this.world.slowAt(p.pos.x, p.pos.z) ? 0.45 : 1); // terreno específico ou água/lago // velocidade base × ARMA (MOVE_MUL) × andar × ADS × agachado × água const wpnMul = MOVE2 ? (MOVE_MUL[p.weapon] !== undefined ? MOVE_MUL[p.weapon] : 0.9) : 1; const maxSp = MOVE2 @@ -4933,7 +4935,11 @@ export class Game { if (moving) { p.stepPhase += dt * sp * 1.6; const prev = Math.sin(p.stepPhase - dt * sp * 1.6), now = Math.sin(p.stepPhase); - if (prev >= 0 && now < 0) this.sfx.step(this.world.slowAt && this.world.slowAt(p.pos.x, p.pos.z) ? 'water' : 'concrete'); + if (prev >= 0 && now < 0) { + const surface = this.world.surfaceAt?.(p.pos.x, p.pos.z) + ?? (this.world.slowAt && this.world.slowAt(p.pos.x, p.pos.z) ? 'water' : 'concrete'); + this.sfx.step(surface); + } } // Aim: real scopes (AWP / Mosin / Rem700) hide the gun and show the scope overlay. // Every other weapon does light iron-sight ADS — the gun stays on screen and the @@ -6252,7 +6258,8 @@ export class Game { // 180° custa ~0,9 s e lê como uma curva de pessoa. const turn = dy * Math.min(1, dt * (BOT_MOVE2 ? 6 : 12)); b.yaw += BOT_MOVE2 ? Math.max(-YAW_CAP * dt, Math.min(YAW_CAP * dt, turn)) : turn; - const bSlow = this.world.slowAt && this.world.slowAt(b.pos.x, b.pos.z) ? 0.5 : 1; // bots também vadear + const bSlow = this.world.speedMulAt?.(b.pos.x, b.pos.z) + ?? (this.world.slowAt && this.world.slowAt(b.pos.x, b.pos.z) ? 0.5 : 1); // bots também respeitam o terreno const px = b.pos.x, pz = b.pos.z; b.pos.x += Math.sin(b.yaw) * BOT_SPEED * bSlow * dt; b.pos.z += Math.cos(b.yaw) * BOT_SPEED * bSlow * dt; diff --git a/public/js/main.js b/public/js/main.js index 06dddff33..bc7952851 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -1579,6 +1579,7 @@ const MAP_DESC = { penitenciaria: 'Rebelião no pátio: celas abertas, concreto gasto, guaritas e barricadas policiais entre três rotas de confronto.', upa_24h: 'Pronto-socorro lotado: salas de verdade, corredor em cruz e treta no fluorescente — 100% interno.', obras_prefeitura: 'Canteiro de obra eterna: terreno ondulado, buracos de escavação, tapumes e a treta do desvio de verba.', + treta_no_gelo: 'Fortaleza congelada de pedra e neve: bastiões octogonais, rampas defensivas e rotas apertadas em volta do bloco central.', }; /* Categoria é LISTA: um mapa pode ser ARENA e COMUNIDADE ao mesmo tempo. * 'AI' entra aqui no dia em que o primeiro mapa de agente chegar — o filtro, @@ -1593,6 +1594,7 @@ const MAP_CATS = { penitenciaria: ['ARENA', 'COMUNIDADE'], upa_24h: ['ARENA', 'COMUNIDADE'], obras_prefeitura: ['ARENA', 'COMUNIDADE'], + treta_no_gelo: ['ARENA', 'COMUNIDADE'], }; const MAP_AUTOR = { praca_poderes: 'Ruben Marcus', loja_h: 'Ruben Marcus', @@ -1600,6 +1602,7 @@ const MAP_AUTOR = { piscina_treta: 'Dalton Fontes', posto_treta: 'Emerson Garrido', parque_treta: 'Ubiracy Santos', velho_oeste: 'Ubiracy Santos', penitenciaria: 'Ubiracy Santos', upa_24h: 'Emerson Garrido', obras_prefeitura: 'Emerson Garrido', + treta_no_gelo: 'Ubiracy Santos', }; const MAP_DATA = { praca_poderes: '19/07/2026', loja_h: '31/07/2026', ferro_velho: '31/07/2026', @@ -1607,6 +1610,7 @@ const MAP_DATA = { piscina_treta: '17/07/2026', posto_treta: '13/08/2026', parque_treta: '17/08/2026', velho_oeste: '17/08/2026', penitenciaria: '17/08/2026', upa_24h: '13/08/2026', obras_prefeitura: '13/08/2026', + treta_no_gelo: '18/08/2026', }; const CAT_DESC = { TODOS: 'O acervo inteiro, do mais jogado ao menos jogado.', diff --git a/public/js/map_gelo.js b/public/js/map_gelo.js new file mode 100644 index 000000000..479e1c801 --- /dev/null +++ b/public/js/map_gelo.js @@ -0,0 +1,250 @@ +import * as THREE from 'three'; + +const HALF_X = 40; +const HALF_Z = 42; + +function canvasTexture(name, base, light, dark, mode, repeatX = 4, repeatY = repeatX) { + const canvas = document.createElement('canvas'); + canvas.width = canvas.height = 256; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = base; + ctx.fillRect(0, 0, 256, 256); + let seed = [...name].reduce((value, char) => (value * 33 + char.charCodeAt(0)) >>> 0, 1977); + const random = () => ((seed = (seed * 1664525 + 1013904223) >>> 0) / 4294967296); + + if (mode === 'stone') { + for (let row = 0; row < 8; row++) { + const y = row * 32; + const offset = row % 2 ? 24 : 0; + ctx.strokeStyle = dark; + ctx.lineWidth = 3; + ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(256, y); ctx.stroke(); + for (let x = offset; x < 256; x += 52) { + ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x, y + 32); ctx.stroke(); + } + } + for (let i = 0; i < 850; i++) { + const alpha = .025 + random() * .11; + ctx.fillStyle = random() > .45 ? `rgba(235,248,255,${alpha})` : `rgba(18,39,55,${alpha})`; + ctx.fillRect(random() * 256, random() * 256, 1 + random() * 7, 1 + random() * 4); + } + } else if (mode === 'snow') { + for (let i = 0; i < 900; i++) { + const alpha = .03 + random() * .12; + ctx.fillStyle = random() > .35 ? `rgba(255,255,255,${alpha})` : `rgba(85,130,158,${alpha})`; + ctx.beginPath(); ctx.arc(random() * 256, random() * 256, 1 + random() * 5, 0, Math.PI * 2); ctx.fill(); + } + } else { + const gradient = ctx.createLinearGradient(0, 0, 256, 256); + gradient.addColorStop(0, light); gradient.addColorStop(.45, base); gradient.addColorStop(1, dark); + ctx.fillStyle = gradient; ctx.fillRect(0, 0, 256, 256); + ctx.strokeStyle = 'rgba(205,239,255,.34)'; ctx.lineWidth = 2; + for (let i = 0; i < 18; i++) { + let x = random() * 256, y = random() * 256; + ctx.beginPath(); ctx.moveTo(x, y); + for (let j = 0; j < 4; j++) { x += random() * 30 - 15; y += random() * 24; ctx.lineTo(x, y); } + ctx.stroke(); + } + } + + const texture = new THREE.CanvasTexture(canvas); + texture.name = name; + texture.colorSpace = THREE.SRGBColorSpace; + texture.wrapS = texture.wrapT = THREE.RepeatWrapping; + texture.repeat.set(repeatX, repeatY); + texture.anisotropy = 8; + return texture; +} + +export function buildTretaNoGelo(scene) { + const root = new THREE.Group(); + root.name = 'treta-no-gelo'; + scene.add(root); + const colliders = [], occluders = [], pickups = []; + const geometries = new Map(); + const boxGeometry = (w, h, d) => { + const key = `${w}:${h}:${d}`; + if (!geometries.has(key)) geometries.set(key, new THREE.BoxGeometry(w, h, d)); + return geometries.get(key); + }; + + const textures = { + stone: canvasTexture('gelo-pedra', '#7894a5', '#b8d0dc', '#3e5d70', 'stone', 5, 3), + snow: canvasTexture('gelo-neve', '#dbeaf0', '#ffffff', '#8db0c2', 'snow', 6, 6), + floor: canvasTexture('gelo-piso', '#8cb7cc', '#d8f2fb', '#416c86', 'ice', 8, 9), + trim: canvasTexture('gelo-friso', '#5d7d92', '#afcedc', '#294657', 'stone', 8, 1), + }; + const materials = { + stone: new THREE.MeshStandardMaterial({ map: textures.stone, bumpMap: textures.stone, bumpScale: .055, color: 0xc7dbe4, roughness: .84 }), + stoneDark: new THREE.MeshStandardMaterial({ map: textures.stone, bumpMap: textures.stone, bumpScale: .04, color: 0x7894a5, roughness: .9 }), + snow: new THREE.MeshStandardMaterial({ map: textures.snow, bumpMap: textures.snow, bumpScale: .035, color: 0xffffff, roughness: .92 }), + ice: new THREE.MeshPhysicalMaterial({ map: textures.floor, bumpMap: textures.floor, bumpScale: .025, color: 0xbfe7f4, roughness: .3, metalness: .04, clearcoat: .45, clearcoatRoughness: .28 }), + trim: new THREE.MeshStandardMaterial({ map: textures.trim, color: 0xa3c0cf, roughness: .76 }), + wood: new THREE.MeshStandardMaterial({ color: 0x5b3c2a, roughness: .88 }), + woodLight: new THREE.MeshStandardMaterial({ color: 0x9a7551, roughness: .82 }), + metal: new THREE.MeshStandardMaterial({ color: 0x526674, metalness: .58, roughness: .5 }), + crystal: new THREE.MeshPhysicalMaterial({ color: 0x8edcf2, emissive: 0x17435a, emissiveIntensity: .3, roughness: .18, metalness: .05, transparent: true, opacity: .82 }), + }; + + function addCollider(x, y, z, w, h, d, tag) { + const collider = { minX: x-w/2, maxX: x+w/2, minY: y, maxY: y+h, minZ: z-d/2, maxZ: z+d/2, tag }; + colliders.push(collider); + return collider; + } + function addBox(w, h, d, material, x, y, z, options = {}) { + const mesh = new THREE.Mesh(boxGeometry(w, h, d), material); + mesh.position.set(x, y + h / 2, z); + if (options.ry) mesh.rotation.y = options.ry; + mesh.name = options.name || ''; + mesh.castShadow = options.cast !== false; + mesh.receiveShadow = true; + root.add(mesh); + if (options.collide !== false) { + const hx = Math.abs(Math.cos(options.ry || 0))*w/2 + Math.abs(Math.sin(options.ry || 0))*d/2; + const hz = Math.abs(Math.sin(options.ry || 0))*w/2 + Math.abs(Math.cos(options.ry || 0))*d/2; + const collider = addCollider(x, y, z, hx*2, h, hz*2, options.tag || options.name); + mesh.userData.collider = collider; + if (h > 1.1) occluders.push(mesh); + } + return mesh; + } + function addOctagon(radius, height, material, x, y, z, name, collide = true) { + const mesh = new THREE.Mesh(new THREE.CylinderGeometry(radius, radius, height, 8), material); + mesh.position.set(x, y + height / 2, z); + mesh.rotation.y = Math.PI / 8; + mesh.name = name; + mesh.castShadow = true; + mesh.receiveShadow = true; + root.add(mesh); + if (collide) { + const collider = addCollider(x, y, z, radius*1.86, height, radius*1.86, name); + mesh.userData.collider = collider; + occluders.push(mesh); + } + return mesh; + } + + scene.background = new THREE.Color(0x9fb9ca); + scene.fog = new THREE.Fog(0xa8bfcd, 62, 135); + const ground = new THREE.Mesh(new THREE.PlaneGeometry(110, 116), materials.ice); + ground.rotation.x = -Math.PI/2; + ground.position.y = -.025; + ground.receiveShadow = true; + ground.name = 'gelo-patio-congelado'; + root.add(ground); + + for (const [w,d,x,z,name] of [ + [82,1.2,0,-HALF_Z,'sul'], [82,1.2,0,HALF_Z,'norte'], + [1.2,84,-HALF_X,0,'oeste'], [1.2,84,HALF_X,0,'leste'], + ]) { + addBox(w, 5.4, d, materials.stone, x, 0, z, { name:`gelo-muro-${name}` }); + addBox(w+.15, .45, d+.18, materials.trim, x, 4.2, z, { name:`gelo-friso-muro-${name}`, collide:false }); + addBox(w+.35, .35, d+.35, materials.snow, x, 5.35, z, { name:`gelo-neve-muro-${name}`, collide:false }); + } + + function fortressPart(id, radius, height, x, z, central = false) { + const group = new THREE.Group(); + group.name = central ? 'gelo-fortaleza-central' : `gelo-fortaleza-bastiao-${id}`; + root.add(group); + const body = addOctagon(radius, height, central ? materials.stoneDark : materials.stone, x, 0, z, `gelo-bloco-${central ? 'central' : id}`); + const lowerTrim = addOctagon(radius+.1, .38, materials.trim, x, .38, z, `gelo-friso-inferior-${id}`, false); + const upperTrim = addOctagon(radius+.16, .48, materials.trim, x, height-.68, z, `gelo-friso-superior-${id}`, false); + const roof = addOctagon(radius+.32, .28, materials.snow, x, height, z, `gelo-telhado-neve-${id}`, false); + for (const object of [body, lowerTrim, upperTrim, roof]) { root.remove(object); group.add(object); } + return group; + } + fortressPart('centro', 10.8, 8.2, 0, 0, true); + [[-23,-21],[23,-21],[-23,21],[23,21]].forEach(([x,z], index) => fortressPart(index, 6.8, 6.4, x, z)); + + const wallSpecs = [ + { id: 'sul', x: -12, z: -8.5, yaw: 0 }, + { id: 'norte', x: 12, z: 8.5, yaw: Math.PI }, + ]; + function iceWall(id, x, z, yaw) { + const group = new THREE.Group(); group.name = `gelo-parede-${id}`; root.add(group); + const body = addBox(7.2,3.2,.9,materials.stone,x,0,z,{ry:yaw,name:`gelo-estrutura-parede-${id}`,tag:`gelo-parede-${id}`}); + const trim = addBox(7.34,.32,1.02,materials.trim,x,2.48,z,{ry:yaw,collide:false,name:`gelo-friso-parede-${id}`}); + const snow = addBox(7.48,.2,1.14,materials.snow,x,3.2,z,{ry:yaw,collide:false,name:`gelo-neve-parede-${id}`}); + for (const object of [body, trim, snow]) { root.remove(object); group.add(object); } + } + wallSpecs.forEach(({ id, x, z, yaw }) => iceWall(id, x, z, yaw)); + + const groundHeightAt = () => 0; + + function crate(id, x, z, yaw = 0) { + const group = new THREE.Group(); group.name = `gelo-cobertura-caixa-${id}`; root.add(group); + const body = addBox(2.35,1.75,2.35,materials.wood,x,0,z,{ry:yaw,name:`gelo-caixa-corpo-${id}`}); + root.remove(body); group.add(body); group.userData.collider = body.userData.collider; + for (const side of [-1,1]) { + const band = addBox(.18,1.82,2.46,materials.metal,x+side*.78*Math.cos(yaw),0,z-side*.78*Math.sin(yaw),{ry:yaw,collide:false,name:`gelo-caixa-cinta-${id}-${side}`}); + root.remove(band); group.add(band); + } + const snow = addBox(2.48,.14,2.48,materials.snow,x,1.72,z,{ry:yaw,collide:false,name:`gelo-caixa-neve-${id}`}); + root.remove(snow); group.add(snow); + } + [[-32,-8,.1],[32,8,.1],[-31,10,-.2],[31,-10,-.2],[-13,-30,.2],[13,30,.2],[-4,-18,-.1],[4,18,-.1]].forEach((p,i)=>crate(i,...p)); + + function crystalCover(id, x, z, scale = 1) { + const group = new THREE.Group(); group.name = `gelo-cobertura-cristal-${id}`; root.add(group); + for (const [dx,dz,h,r] of [[0,0,2.8,.72],[-.7,.25,1.9,.48],[.65,.35,2.2,.55]]) { + const mesh = new THREE.Mesh(new THREE.ConeGeometry(r*scale, h*scale, 5), materials.crystal); + mesh.position.set(x+dx*scale,h*scale/2,z+dz*scale); mesh.rotation.y = id*.7+dx; + mesh.name=`gelo-cristal-${id}`; mesh.castShadow=true; group.add(mesh); + } + group.userData.collider = addCollider(x,0,z,2.6*scale,2.8*scale,2.2*scale,`cristal-${id}`); + occluders.push(group); + } + [[-17,0,1],[17,0,1],[0,-28,1.05],[0,28,1.05]].forEach((p,i)=>crystalCover(i,...p)); + + for (const [id,x,z,yaw] of [['so',-27,-33,.2],['se',27,-33,-.2],['no',-27,33,-.2],['ne',27,33,.2]]) { + addBox(7.2,1.4,.9,materials.stone,x,0,z,{ry:yaw,name:`gelo-cobertura-barreira-${id}`}); + addBox(7.35,.18,1.02,materials.snow,x,1.36,z,{ry:yaw,collide:false,name:`gelo-neve-barreira-${id}`}); + } + + const gunMaterials = { dark: materials.metal, wood: materials.woodLight }; + function gun(kind, x, z, yaw) { + const group = new THREE.Group(); group.name = `arma-gelo-${kind}`; group.position.set(x,.12,z); group.rotation.y=yaw; root.add(group); + const long = !['pistol','deagle'].includes(kind); + const body = new THREE.Mesh(boxGeometry(.15,.13,long?.92:.42),gunMaterials.dark); body.position.y=.12; group.add(body); + if (long) { const barrel = new THREE.Mesh(boxGeometry(.07,.07,.56),gunMaterials.dark); barrel.position.set(0,.14,-.62); group.add(barrel); } + const grip = new THREE.Mesh(boxGeometry(.12,.27,.15),gunMaterials.wood); grip.position.set(0,-.02,long?.22:.1); group.add(grip); + pickups.push({x,z,kind,weapon:kind,readyAt:0,mesh:group}); + } + const arsenal = ['ak','m4','awp','shotgun','mp5','smg','deagle','pistol']; + for (let i=0;i<12;i++) { + const x=-16+i*(32/11), kind=arsenal[i%arsenal.length]; + gun(kind,x,-13+(i%2)*2.5,.12*i); gun(kind,-x,13-(i%2)*2.5,Math.PI+.12*i); + } + + const blocked = (x,z,pad=.72) => colliders.some((c)=>x>c.minX-pad&&xc.minZ-pad&&z[]); + for(let i=0;i1, speedMulAt:()=>.55, surfaceAt:()=> 'ice', + spawns:{E:[-18,-6,6,18].map(x=>({x,z:-36,yaw:0})),B:[18,6,-6,-18].map(x=>({x,z:36,yaw:Math.PI}))}, + ctfPoints:[ + {id:'E',x:0,z:-34,label:'PORTÃO SUL'},{id:'O',x:-31,z:0,label:'BASTIÃO OESTE'}, + {id:'L',x:31,z:0,label:'BASTIÃO LESTE'},{id:'B',x:0,z:34,label:'PORTÃO NORTE'}, + ], + waypoints:{nodes,adj},nearestWaypoint,findPath,bounds, + }; +} diff --git a/public/js/maps.js b/public/js/maps.js index aa52ec31f..9417da507 100644 --- a/public/js/maps.js +++ b/public/js/maps.js @@ -11,6 +11,7 @@ import { buildAtacadao, ATACADAO_PROPS } from './map_atacadao.js'; import { buildParque } from './map_parque.js'; import { buildVelhoOeste } from './map_velho_oeste.js'; import { buildPenitenciaria } from './map_penitenciaria.js'; +import { buildTretaNoGelo } from './map_gelo.js'; /* IDS SEM NOME DE COUNTER-STRIKE (rodada de 11/08). ═══════════════════════════════════════════════════════════════════════════════════ @@ -80,6 +81,7 @@ export const MAPS = { parque_treta: { name: 'Parque da Treta', build: buildParque, ctfMode: true }, velho_oeste: { name: 'Velho Oeste da Treta', build: buildVelhoOeste, ctfMode: true }, penitenciaria: { name: 'Penitenciária da Treta', build: buildPenitenciaria, ctfMode: true }, + treta_no_gelo: { name: 'Treta no Gelo', build: buildTretaNoGelo }, }; export const MAP_IDS = Object.keys(MAPS); export const DEFAULT_MAP = 'praca_poderes'; diff --git a/src/data/jogo.ts b/src/data/jogo.ts index 56eac74d6..b152fec7d 100644 --- a/src/data/jogo.ts +++ b/src/data/jogo.ts @@ -128,7 +128,7 @@ export interface Mapa { ctf: boolean; } -/** 5 arenas jogáveis - a ordem é a do registro (public/js/maps.js, objeto MAPS). */ +/** Arenas jogáveis - a ordem é a do registro (public/js/maps.js, objeto MAPS). */ export const MAPAS: Mapa[] = [ { id: 'praca_poderes', nome: 'Praça dos Três Poderes', modo: 'Rounds · padrão', ctf: false, @@ -171,6 +171,13 @@ export const MAPAS: Mapa[] = [ 'impede a rua virar corredor de sniper. Quatro bandeiras: campinho, bar de esquina, ponto de ' + 'ônibus e praça do baile.', }, + { + id: 'treta_no_gelo', nome: 'Treta no Gelo', modo: 'Rounds · padrão', ctf: false, + resumo: 'Fortaleza congelada com bastiões octogonais e corredores de neve.', + detalhe: 'O bloco central corta as linhas de tiro e obriga a escolher entre os corredores curtos, ' + + 'as rampas defensivas e os flancos junto aos quatro bastiões. Pedra congelada, cristais, caixas ' + + 'e barreiras cobertas de neve criam cobertura sem esconder os pontos de referência da arena.', + }, ]; export interface Personagem { faccao: string; nome: string; blurb: string; } diff --git a/src/pages/maps.astro b/src/pages/maps.astro index 9eb0acea6..bb50e3cc2 100644 --- a/src/pages/maps.astro +++ b/src/pages/maps.astro @@ -52,6 +52,13 @@ const MAP_EN: Record 'that keeps the street from becoming a sniper corridor. Four flags: dirt pitch, corner bar, ' + 'bus stop and baile square.', }, + treta_no_gelo: { + nome: 'Fight on Ice', + resumo: 'A frozen fortress with octagonal bastions and snow-covered corridors.', + detalhe: 'The central block breaks long sightlines and forces a choice between short corridors, ' + + 'defensive ramps and flanks around the four bastions. Frozen stone, crystals, crates and ' + + 'snow-covered barriers create cover without hiding the arena landmarks.', + }, }; const en = (id: string) => MAP_EN[id]; diff --git a/tools/eval/treta-gelo-check.mjs b/tools/eval/treta-gelo-check.mjs new file mode 100644 index 000000000..e63310ecf --- /dev/null +++ b/tools/eval/treta-gelo-check.mjs @@ -0,0 +1,141 @@ +import { readFileSync } from 'node:fs'; +import { bootGame, MAPS, THREE, initTextures } from './harness.mjs'; + +const MUTANTE = (process.argv.find((arg) => arg.startsWith('--mutante=')) || '').split('=')[1] || ''; +const entry = MAPS.treta_no_gelo; +if (!entry) { + console.error('GELO FALHA — treta_no_gelo não está no registro de mapas'); + process.exit(1); +} + +const scene = new THREE.Scene(); +const harnessTextures = await initTextures(); +const world = entry.build(scene, harnessTextures); +const objects = []; +world.root.traverse((object) => objects.push(object)); +const named = (prefix) => objects.filter((object) => object.name?.startsWith(prefix)); + +if (MUTANTE === 'sem-paredes') for (const object of [...named('gelo-parede-'), ...named('gelo-estrutura-parede-')]) object.name = 'mutante-sem-parede'; +if (MUTANTE === 'sem-fortaleza') for (const object of named('gelo-fortaleza-')) object.name = 'mutante-sem-fortaleza'; +if (MUTANTE === 'sem-texturas') { + for (const object of objects) for (const material of Array.isArray(object.material) ? object.material : [object.material]) { + if (material?.map) material.map.name = 'mutante-sem-textura'; + } +} +if (MUTANTE === 'sem-cobertura') for (const object of named('gelo-cobertura-')) object.name = 'mutante-sem-cobertura'; +if (MUTANTE === 'parede-cor') for (const object of named('gelo-estrutura-parede-')) object.material.map.name = 'mutante-cor-parede'; +if (MUTANTE === 'gelo-lento') world.speedMulAt = () => .45; +if (MUTANTE === 'gelo-rapido') world.speedMulAt = () => 1; +if (MUTANTE === 'parede-sem-uv') for (const object of named('gelo-estrutura-parede-')) object.geometry.deleteAttribute('uv'); +if (MUTANTE === 'passo-concreto') world.surfaceAt = () => 'concrete'; +if (MUTANTE === 'parede-sem-colisao') world.colliders = world.colliders.filter((collider) => !String(collider.tag || '').startsWith('gelo-parede-')); + +const textures = new Set(); +for (const object of objects) for (const material of Array.isArray(object.material) ? object.material : [object.material]) { + if (material?.map?.name) textures.add(material.map.name); +} + +const fortress = named('gelo-fortaleza-'); +const ramps = named('gelo-rampa-'); +const iceWalls = named('gelo-estrutura-parede-'); +const cover = named('gelo-cobertura-'); +const architectureOk = named('gelo-fortaleza-central').length === 1 + && named('gelo-fortaleza-bastiao-').length === 4 + && fortress.length >= 5 && iceWalls.length === 2 && ramps.length === 0; +const textureOk = ['gelo-pedra', 'gelo-neve', 'gelo-piso'].every((name) => textures.has(name)); +const gameplayOk = cover.length >= 12 && world.colliders.length >= 25 + && world.pickups.length >= 20 && world.ctfPoints?.length === 4; +const wallReplacementOk = iceWalls.length === 2 && ramps.length === 0 + && world.colliders.filter((collider) => String(collider.tag || '').startsWith('gelo-parede-')).length === 2; + +const spawnE = world.spawns?.E?.[0], spawnB = world.spawns?.B?.[0]; +let route = []; +if (spawnE && spawnB) route = world.findPath(world.nearestWaypoint(spawnE.x, spawnE.z), world.nearestWaypoint(spawnB.x, spawnB.z)); +const navigationOk = world.waypoints?.nodes?.length >= 100 && route.length >= 8 + && world.waypoints.adj.every((line) => Array.isArray(line)); +let previewSource = readFileSync('tools/eval/g2ui-map-previews.mjs', 'utf8'); +if (MUTANTE === 'preview-com-arma') previewSource = previewSource.replaceAll('g.vmScene.visible = false', 'g.vmScene.visible = true').replaceAll('mute(d.mesh)', 'd.mesh.visible = true'); +const cleanPreviewOk = /g\.vmScene\.visible = false/.test(previewSource) + && /for \(const d of g\.drops\)[\s\S]{0,80}mute\(d\.mesh\)/.test(previewSource) + && /#hud,#hud-shortcuts\{display:none!important\}/.test(previewSource); +const wallUvRatios = iceWalls.map((mesh) => { + const uv = mesh.geometry.getAttribute('uv'); + const index = mesh.geometry.index?.array; + if (!uv || !index) return 0; + let visible = 0, mapped = 0; + for (let offset = 0; offset < index.length; offset += 3) { + visible++; + const a = index[offset], b = index[offset + 1], c = index[offset + 2]; + const area = Math.abs((uv.getX(b)-uv.getX(a))*(uv.getY(c)-uv.getY(a)) + - (uv.getY(b)-uv.getY(a))*(uv.getX(c)-uv.getX(a))) / 2; + if (area > .01) mapped++; + } + return visible ? mapped / visible : 0; +}); +const wallUvOk = wallUvRatios.length === 2 && wallUvRatios.every((ratio) => ratio === 1); +const wallMaterialOk = iceWalls.every((mesh) => mesh.geometry.type === 'BoxGeometry' + && mesh.material?.map?.name === 'gelo-pedra'); +const game = bootGame('treta_no_gelo', { textures: harnessTextures, ctf: false, seed: 4242 }); +game.state = 'live'; +if (MUTANTE === 'parede-sem-colisao') game.world.colliders = game.world.colliders.filter((collider) => !String(collider.tag || '').startsWith('gelo-parede-')); +const wallStops = [ + { x: -12, z: -8.5, yaw: 0 }, + { x: 12, z: 8.5, yaw: Math.PI }, +].map((wallSpec) => { + const localStart = -5; + const startX = wallSpec.x + Math.sin(wallSpec.yaw) * localStart; + const startZ = wallSpec.z + Math.cos(wallSpec.yaw) * localStart; + const player = game.player; + player.pos.set(startX, 0, startZ); + player.vel.set(0, 0, 0); player.yaw = wallSpec.yaw + Math.PI; player.grounded = true; + game.keys.KeyW = true; + for (let frame = 0; frame < 240; frame++) { game.time += 1 / 60; game._updatePlayer(1 / 60); } + game.keys.KeyW = false; + const dx = player.pos.x - wallSpec.x, dz = player.pos.z - wallSpec.z; + return Math.sin(wallSpec.yaw) * dx + Math.cos(wallSpec.yaw) * dz; +}); +const wallsBlockingOk = wallReplacementOk && wallMaterialOk && wallStops.every((localZ) => localZ < 0); +const walkDistance = (speedMul) => { + const player = game.player; + game.world.speedMulAt = speedMul; + player.pos.set(-34, 0, -28); player.vel.set(0, 0, 0); player.yaw = Math.PI; player.grounded = true; + game.keys.KeyW = true; + const startZ = player.pos.z; + for (let frame = 0; frame < 180; frame++) { game.time += 1 / 60; game._updatePlayer(1 / 60); } + game.keys.KeyW = false; + return player.pos.z - startZ; +}; +const slowDistance = walkDistance(() => .45); +const iceSpeedMul = MUTANTE === 'gelo-lento' ? () => .45 : MUTANTE === 'gelo-rapido' ? () => 1 : world.speedMulAt; +const iceDistance = walkDistance(iceSpeedMul); +const icePaceRatio = iceDistance / slowDistance; +const icePaceOk = typeof world.speedMulAt === 'function' && icePaceRatio >= 1.15 && icePaceRatio <= 1.30; +if (MUTANTE === 'passo-concreto') game.world.surfaceAt = () => 'concrete'; +const stepSurfaces = []; +game.sfx = { step: (surface) => stepSurfaces.push(surface) }; +game.player.pos.set(-34, 0, -28); game.player.vel.set(0, 0, 0); game.player.yaw = Math.PI; +game.player.grounded = true; game.player.stepPhase = 0; game.keys.KeyW = true; +for (let frame = 0; frame < 120; frame++) { game.time += 1 / 60; game._updatePlayer(1 / 60); } +game.keys.KeyW = false; +const { Sfx } = await import('../../public/js/audio.js'); +const iceBursts = []; +const synth = Object.create(Sfx.prototype); +synth.pack = null; synth.ensure = () => {}; synth._burst = (...args) => iceBursts.push(args); +synth.step('ice'); +const iceStepOk = typeof world.surfaceAt === 'function' && world.surfaceAt(0, 0) === 'ice' + && stepSurfaces.length >= 2 && stepSurfaces.every((surface) => surface === 'ice') + && iceBursts.length >= 2 && iceBursts.some((burst) => burst[4] === 'bandpass'); +game.dispose(); + +console.log(`GELO1 ${architectureOk ? 'PASSA' : 'FALHA'} — fortaleza central, quatro bastiões e duas paredes internas`); +console.log(`GELO2 ${textureOk ? 'PASSA' : 'FALHA'} — pedra congelada, neve e piso de gelo texturizados`); +console.log(`GELO3 ${gameplayOk ? 'PASSA' : 'FALHA'} — ${cover.length} coberturas · ${world.colliders.length} colisores · ${world.pickups.length} armas · ${world.ctfPoints?.length || 0} objetivos`); +console.log(`GELO4 ${navigationOk ? 'PASSA' : 'FALHA'} — ${world.waypoints?.nodes?.length || 0} nós · rota entre spawns com ${route.length} passos`); +console.log(`GELO5 ${cleanPreviewOk ? 'PASSA' : 'FALHA'} — preview oculta HUD, viewmodel e armas do chão`); +console.log(`GELO6 ${wallsBlockingOk ? 'PASSA' : 'FALHA'} — paredes de gelo bloquearam o jogador em ${wallStops.map((localZ) => localZ.toFixed(2)).join('/')} m locais`); +console.log(`GELO7 ${icePaceOk ? 'PASSA' : 'FALHA'} — passada no gelo ${icePaceRatio.toFixed(2)}× a passada lenta (${iceDistance.toFixed(2)} m em 3 s)`); +console.log(`GELO8 ${wallUvOk ? 'PASSA' : 'FALHA'} — cobertura UV das paredes ${wallUvRatios.map((ratio) => (ratio*100).toFixed(0)+'%').join('/')}`); +console.log(`GELO9 ${iceStepOk ? 'PASSA' : 'FALHA'} — ${stepSurfaces.length} passos receberam superfície de gelo · ${iceBursts.length} camadas procedurais`); +console.log(`GELO10 ${wallReplacementOk ? 'PASSA' : 'FALHA'} — ${iceWalls.length} paredes substituem ${ramps.length} rampas · colisão sólida nos dois lados`); + +process.exit(architectureOk && textureOk && gameplayOk && navigationOk && cleanPreviewOk && wallsBlockingOk && icePaceOk && wallUvOk && iceStepOk && wallReplacementOk ? 0 : 1);