diff --git a/ARCH.generated.md b/ARCH.generated.md index 78d40edb..11257229 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** | 59 arquivos `.js`, 41.159 linhas · Three.js `r160` vendorizado | ES modules servidos crus, **zero build**, sem dependência de runtime | +| `public/` | o **jogo** | 60 arquivos `.js`, 41.826 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** | 300 scripts em `tools/eval/`, 77 em `tools/` | node puro: sobe o jogo real sem browser | +| `tools/` | o **arnês** | 301 scripts em `tools/eval/`, 77 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. @@ -28,7 +28,7 @@ Comandos e scripts atuais do quality gate: 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:ambience-registry 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:charhard eval:charpbr eval:motoca-visual eval:camera-grip eval:pilot-system eval:pilot-grip eval:char-thumbnail eval:asset-integrity eval:gltf-validator eval:props-acervo eval:propsuv1 eval:character-voice eval:audio-pack-character-voice eval:cinematic-ui eval:grafite-editorial eval:map-source eval:map-new eval:campo-contract eval:lajes-rooftop eval:lajes-visual eval:lajes-authored eval:lajes-spatial eval:lajes-gap eval:lajes-circuito eval:lajes-antitrap eval:mansao-water eval:mansao-garden eval:corrego-contract eval:corrego-water eval:look eval:mansao-ocean eval:wind eval:softparticles eval:escala-favela eval:escadao-contract eval:slice-abilities eval:faction-registry eval:mapview eval:devport spec:check skills:check eval:pickuparma eval:mutcega eval:autofix eval:deploygate eval:portaointeiro eval:wfsecret eval:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam ``` -`package.json` tem **186 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 **187 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 d489e9b4..3a4cafbd 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 41.159 linhas em 59 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| Código do jogo | 41.826 linhas em 60 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | | `game.js` | **7.260** linhas | `wc -l public/js/game.js` | | `main.js` | 2.812 linhas | `wc -l public/js/main.js` | | Armas com GLB | 27 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | @@ -42,9 +42,9 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | Props em GLB | 151 | `git ls-files 'public/models/props/*.glb' \| wc -l` | | Clipes de animação versionados | 681 | `git ls-files public/models/anims \| wc -l` | | Personagens jogáveis | 62, em 10 facções | array `CHARACTERS` de `characters.js` | -| Mapas no registro | 17 | objeto `MAPS` de `maps.js` | +| Mapas no registro | 18 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 16 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 300 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 301 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 77 | `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.184` | `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 8480b985..d05a97f1 100644 --- a/STATUS.md +++ b/STATUS.md @@ -3,9 +3,9 @@ - **Versão:** `2.0.0-alpha.184` -- **Conteúdo jogável:** 10 facções, 62 personagens, 17 mapas e 27 armas com GLB -- **Código do jogo:** 41.159 linhas em 59 módulos JavaScript -- **Automação:** 186 comandos npm, 300 scripts de avaliação e 77 scripts de pipeline +- **Conteúdo jogável:** 10 facções, 62 personagens, 18 mapas e 27 armas com GLB +- **Código do jogo:** 41.826 linhas em 60 módulos JavaScript +- **Automação:** 187 comandos npm, 301 scripts de avaliação e 77 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 044e6f5d..3492cf89 100644 --- a/docs/docs/arquitetura.md +++ b/docs/docs/arquitetura.md @@ -70,7 +70,7 @@ Tamanho dos arquivos que o `gen-arch.mjs` indexa — bloco gerado, regenerado po | `public/js/weapons.js` | 356 | | `public/js/springs.js` | 260 | -Total de `public/js/`: **41.159 linhas em 59 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/`: **41.826 linhas em 60 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 20d9de6d..e8929eb1 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 Marcus`, `Ruben`, `Emerson Garrido`, `rubenmarcus`, `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 Marcus`, `Ruben`, `rubenmarcus`, `Emerson Garrido`, `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)` @@ -265,8 +265,9 @@ O registro, gerado do `MAPS` de `public/js/maps.js`: | `parque_treta` | Parque da Treta | **captura** | `map_parque.js` | 426 | | `velho_oeste` | Velho Oeste da Treta | **captura** | `map_velho_oeste.js` | 449 | | `penitenciaria` | Penitenciária da Treta | **captura** | `map_penitenciaria.js` | 264 | +| `amazonia` | Treta na Amazônia | **captura** | `map_amazonia.js` | 501 | -**17 mapas registrados** — 2 abrem em rodadas e 15 em captura. `ctfMode` **abre** o mapa em captura, não prende: o jogador troca no menu (é a `MOD1`). Há 20 arquivos `map_*.js` em `public/js/` — arquivo no disco **não** implica mapa jogável. +**18 mapas registrados** — 2 abrem em rodadas e 16 em captura. `ctfMode` **abre** o mapa em captura, não prende: o jogador troca no menu (é a `MOD1`). Há 21 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 66039d5b..7de09afb 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -39,7 +39,7 @@ esta página envelhecia no primeiro commit — ver | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 41.159 linhas em 59 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| Código do jogo | 41.826 linhas em 60 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | | `game.js` | **7.260** linhas | `wc -l public/js/game.js` | | `main.js` | 2.812 linhas | `wc -l public/js/main.js` | | Armas com GLB | 27 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | @@ -47,9 +47,9 @@ esta página envelhecia no primeiro commit — ver | Props em GLB | 151 | `git ls-files 'public/models/props/*.glb' \| wc -l` | | Clipes de animação versionados | 681 | `git ls-files public/models/anims \| wc -l` | | Personagens jogáveis | 62, em 10 facções | array `CHARACTERS` de `characters.js` | -| Mapas no registro | 17 | objeto `MAPS` de `maps.js` | +| Mapas no registro | 18 | objeto `MAPS` de `maps.js` | | Arnêses visuais em HTML | 16 | `git ls-files 'public/*.html' \| wc -l` | -| Scripts do arnês | 300 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Scripts do arnês | 301 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Scripts de pipeline | 77 | `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.184` | `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 | 10 · 62 (B 9 · C 9 · E 8 · F 9 · M 9 · N 3 · O 2 · R 1 · T 3 · U 9) | `CHARACTERS` | -| Mapas no menu | 17 — 2 abrem em rodadas, **15 em captura** | `MAPS` / `ctfMode` | +| Mapas no menu | 18 — 2 abrem em rodadas, **16 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` | @@ -238,8 +238,9 @@ Os mapas registrados hoje, e em que modo cada um abre: | `parque_treta` | Parque da Treta | **captura** | `map_parque.js` | 426 | | `velho_oeste` | Velho Oeste da Treta | **captura** | `map_velho_oeste.js` | 449 | | `penitenciaria` | Penitenciária da Treta | **captura** | `map_penitenciaria.js` | 264 | +| `amazonia` | Treta na Amazônia | **captura** | `map_amazonia.js` | 501 | -**17 mapas registrados** — 2 abrem em rodadas e 15 em captura. `ctfMode` **abre** o mapa em captura, não prende: o jogador troca no menu (é a `MOD1`). Há 20 arquivos `map_*.js` em `public/js/` — arquivo no disco **não** implica mapa jogável. +**18 mapas registrados** — 2 abrem em rodadas e 16 em captura. `ctfMode` **abre** o mapa em captura, não prende: o jogador troca no menu (é a `MOD1`). Há 21 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` @@ -283,7 +284,7 @@ E os dois quality gates, com a lista exata do que cada um roda — direto do `pa 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:ambience-registry 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:charhard eval:charpbr eval:motoca-visual eval:camera-grip eval:pilot-system eval:pilot-grip eval:char-thumbnail eval:asset-integrity eval:gltf-validator eval:props-acervo eval:propsuv1 eval:character-voice eval:audio-pack-character-voice eval:cinematic-ui eval:grafite-editorial eval:map-source eval:map-new eval:campo-contract eval:lajes-rooftop eval:lajes-visual eval:lajes-authored eval:lajes-spatial eval:lajes-gap eval:lajes-circuito eval:lajes-antitrap eval:mansao-water eval:mansao-garden eval:corrego-contract eval:corrego-water eval:look eval:mansao-ocean eval:wind eval:softparticles eval:escala-favela eval:escadao-contract eval:slice-abilities eval:faction-registry eval:mapview eval:devport spec:check skills:check eval:pickuparma eval:mutcega eval:autofix eval:deploygate eval:portaointeiro eval:wfsecret eval:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam ``` -`package.json` tem **186 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 **187 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 25f5d1e7..06a14ed6 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.281 linhas**, **65 identificadores de invariante declarados** (`put()`), dos quais **28** têm caminho de `skip()` declarado. -- O arnês inteiro são **300 scripts** em `tools/eval/` (`.mjs` + `.py`), mais **77 scripts** de pipeline em `tools/`. +- O arnês inteiro são **301 scripts** em `tools/eval/` (`.mjs` + `.py`), mais **77 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 2617cc0b..a58be19e 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** | 59 arquivos | +| Jogo | ES modules vanilla, **zero build** | 60 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` | @@ -127,7 +127,7 @@ hipotético: | **Meshy** | props 3D por texto (alternativa) e rig | `tools/gen-asset.mjs --provider meshy` | `MESHY_API_KEY` | | **OpenRouter** | arte 2D (cartaz de facção, wallpaper, splash) | `tools/gen-image.mjs` | `OPENROUTER_API_KEY` | -`mint-assets.json` registra **65 assets** gerados via Mint (19 `openrouter-image-concept` · 2 `openrouter-concept-tripo-blender-model` · 5 `imagegen-concept` · 30 `mint-model` · 2 `tripo3d-task` · 2 `openrouter-concept-meshy-model` · 5 `mint-asset-pack`), cada um com `assetId`, `chatUrl` e notas do que deu errado na tentativa anterior. +`mint-assets.json` registra **74 assets** gerados via Mint (19 `openrouter-image-concept` · 2 `openrouter-concept-tripo-blender-model` · 5 `imagegen-concept` · 30 `mint-model` · 2 `tripo3d-task` · 2 `openrouter-concept-meshy-model` · 5 `mint-asset-pack` · 9 `mint-chat`), cada um com `assetId`, `chatUrl` e notas do que deu errado na tentativa anterior. As três chaves de API vivem em `.env` na raiz — **gitignored, modo 600, nunca em `argv`** (argv vaza no `ps` de qualquer processo da máquina). Sem elas o jogo roda igual: o pipeline de geração é offline, o resultado é que entra no repositório. 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 f0ea18ae..7e208838 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md @@ -77,7 +77,7 @@ Size of the files `gen-arch.mjs` indexes — a generated block, regenerated by | `public/js/weapons.js` | 356 | | `public/js/springs.js` | 260 | -Total in `public/js/`: **41,159 lines in 59 files**. The symbol-to-line index lives in `tools/eval/ARCH.md`. +Total in `public/js/`: **41,826 lines in 60 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 9eec8f33..16a85978 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 Marcus`, `Ruben`, `Emerson Garrido`, `rubenmarcus`, `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 Marcus`, `Ruben`, `rubenmarcus`, `Emerson Garrido`, `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)` @@ -268,8 +268,9 @@ The registry, generated from the `MAPS` of `public/js/maps.js`: | `parque_treta` | Parque da Treta | **capture** | `map_parque.js` | 426 | | `velho_oeste` | Velho Oeste da Treta | **capture** | `map_velho_oeste.js` | 449 | | `penitenciaria` | Penitenciária da Treta | **capture** | `map_penitenciaria.js` | 264 | +| `amazonia` | Treta na Amazônia | **capture** | `map_amazonia.js` | 501 | -**17 registered maps** - 2 open in rounds and 15 in capture. `ctfMode` sets the initial mode; it does not lock it. There are 20 `map_*.js` files on disk, so a file alone does **not** make a map playable. +**18 registered maps** - 2 open in rounds and 16 in capture. `ctfMode` sets the initial mode; it does not lock it. There are 21 `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 d5f1adac..51dd54f2 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md @@ -41,7 +41,7 @@ this page was aging at the very first commit — see | What | How much | Where to check | |---|---:|---| -| Game code | 41,159 lines in 59 files | `git ls-files public/js/*.js \| xargs wc -l` | +| Game code | 41,826 lines in 60 files | `git ls-files public/js/*.js \| xargs wc -l` | | `game.js` | **7,260** lines | `wc -l public/js/game.js` | | `main.js` | 2,812 lines | `wc -l public/js/main.js` | | Weapons with GLB | 27 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | @@ -49,9 +49,9 @@ this page was aging at the very first commit — see | Props in GLB | 151 | `git ls-files 'public/models/props/*.glb' \| wc -l` | | Versioned animation clips | 681 | `git ls-files public/models/anims \| wc -l` | | Playable characters | 62, in 10 factions | `CHARACTERS` array in `characters.js` | -| Maps in the registry | 17 | `MAPS` object in `maps.js` | +| Maps in the registry | 18 | `MAPS` object in `maps.js` | | Visual harnesses in HTML | 16 | `git ls-files 'public/*.html' \| wc -l` | -| Harness scripts | 300 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | +| Harness scripts | 301 | `git ls-files 'tools/eval/*.mjs' 'tools/eval/*.py' \| wc -l` | | Pipeline scripts | 77 | `git ls-files 'tools/*.mjs' \| wc -l` | | Written entry tasks | 26 | `git ls-files 'docs/issues/[0-9]*.md' \| wc -l` | | Version | `2.0.0-alpha.184` | `public/js/version.js` and `package.json` (match) | @@ -68,7 +68,7 @@ And the match rules that move around the most, all read from the constants in | Rule | Value | Constant | |---|---|---| | Factions · characters | 10 · 62 (B 9 · C 9 · E 8 · F 9 · M 9 · N 3 · O 2 · R 1 · T 3 · U 9) | `CHARACTERS` | -| Maps in the menu | 17 - 2 open in rounds, **15 in capture** | `MAPS` / `ctfMode` | +| Maps in the menu | 18 - 2 open in rounds, **16 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` | @@ -240,8 +240,9 @@ The maps registered today, and which mode each one opens in: | `parque_treta` | Parque da Treta | **capture** | `map_parque.js` | 426 | | `velho_oeste` | Velho Oeste da Treta | **capture** | `map_velho_oeste.js` | 449 | | `penitenciaria` | Penitenciária da Treta | **capture** | `map_penitenciaria.js` | 264 | +| `amazonia` | Treta na Amazônia | **capture** | `map_amazonia.js` | 501 | -**17 registered maps** - 2 open in rounds and 15 in capture. `ctfMode` sets the initial mode; it does not lock it. There are 20 `map_*.js` files on disk, so a file alone does **not** make a map playable. +**18 registered maps** - 2 open in rounds and 16 in capture. `ctfMode` sets the initial mode; it does not lock it. There are 21 `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` @@ -285,7 +286,7 @@ And the two gates, with the exact list of what each one runs — straight from ` 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:ambience-registry 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:charhard eval:charpbr eval:motoca-visual eval:camera-grip eval:pilot-system eval:pilot-grip eval:char-thumbnail eval:asset-integrity eval:gltf-validator eval:props-acervo eval:propsuv1 eval:character-voice eval:audio-pack-character-voice eval:cinematic-ui eval:grafite-editorial eval:map-source eval:map-new eval:campo-contract eval:lajes-rooftop eval:lajes-visual eval:lajes-authored eval:lajes-spatial eval:lajes-gap eval:lajes-circuito eval:lajes-antitrap eval:mansao-water eval:mansao-garden eval:corrego-contract eval:corrego-water eval:look eval:mansao-ocean eval:wind eval:softparticles eval:escala-favela eval:escadao-contract eval:slice-abilities eval:faction-registry eval:mapview eval:devport spec:check skills:check eval:pickuparma eval:mutcega eval:autofix eval:deploygate eval:portaointeiro eval:wfsecret eval:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam ``` -`package.json` has **186 scripts**; the reason behind each one lives in `SCRIPTS.md`. +`package.json` has **187 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 65c8f779..16768443 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,281 lines**, **65 declared invariant identifiers**, with **28** declared `skip()` paths. -- The harness contains **300 scripts** in `tools/eval/`, plus **77 pipeline scripts** in `tools/`. +- The harness contains **301 scripts** in `tools/eval/`, plus **77 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 a3a39e34..433d9a70 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** | 59 files | +| Game | vanilla ES modules, **zero build** | 60 files | | Site | **Astro** with SSR | `^7.1.1` | | Hosting | **Vercel** adapter | `^11.0.6` | | Database | **managed Postgres** (RLS; private schema) | `^2.110.7` | @@ -126,7 +126,7 @@ hypothetical one: | **Meshy** | 3D props and rigging | `tools/gen-asset.mjs --provider meshy` | `MESHY_API_KEY` | | **OpenRouter** | 2D art | `tools/gen-image.mjs` | `OPENROUTER_API_KEY` | -`mint-assets.json` records **65 assets** generated through Mint (19 `openrouter-image-concept` · 2 `openrouter-concept-tripo-blender-model` · 5 `imagegen-concept` · 30 `mint-model` · 2 `tripo3d-task` · 2 `openrouter-concept-meshy-model` · 5 `mint-asset-pack`). +`mint-assets.json` records **74 assets** generated through Mint (19 `openrouter-image-concept` · 2 `openrouter-concept-tripo-blender-model` · 5 `imagegen-concept` · 30 `mint-model` · 2 `tripo3d-task` · 2 `openrouter-concept-meshy-model` · 5 `mint-asset-pack` · 9 `mint-chat`). API keys live in the gitignored root `.env`; generation is offline and the game runs without them. diff --git a/mint-assets.json b/mint-assets.json index 53ec2ba4..8e7523b7 100644 --- a/mint-assets.json +++ b/mint-assets.json @@ -1328,16 +1328,214 @@ "displayName": "Sleepy Brown Rodent (capivara do córrego)", "notes": "BUG-57 fauna fy_corrego. Mint text-to-3D (projeto First Project, 18/08/2026); otimizado em tools/optimize-ambient-fauna.mjs (WebP 256², dedup/prune) a partir de references/glb/capivara_corrego_mint.glb. Estático (idem jacaré). Escala de mundo: 0,998 m comprimento no GLB; integrar com scale ~1.05 => ~1,05 m comp × 0,61 m altura de bbox (plans/21-FAUNA-CORREGO.md). Ficha e referências idem jacaré." }, - "processing": { - "provider": "mint", - "model": "mint text-to-3d (normalized original_glb)", - "tools": [ - "mint.gg", - "tools/optimize-ambient-fauna.mjs" + "processing": { + "provider": "mint", + "model": "mint text-to-3d (normalized original_glb)", + "tools": [ + "mint.gg", + "tools/optimize-ambient-fauna.mjs" + ], + "finalSha256": "d3ab0c0a22bd5e8072e75ec0ce43ccfeefcd0a2eff1160c0375b9dfc48baf227" + } + }, + "boto-amazonia": { + "artifactType": "model/gltf-binary", + "files": [ + "public/models/ambient/boto_amazonia.glb" ], - "finalSha256": "d3ab0c0a22bd5e8072e75ec0ce43ccfeefcd0a2eff1160c0375b9dfc48baf227" - } - }, + "source": { + "kind": "mint-chat", + "assetId": "ks7feskg5kcssx8v92rb5h5hwd8d53k4", + "chatUrl": "https://mint.gg/chat/ph76cqeq91vd8ktpat7ents25s8d5zqw", + "displayName": "Pink River Dolphin (boto do igarapé)", + "notes": "Fauna viva do mapa amazonia (PR #439): boto-cinza rosado low-poly nadando no igarapé. Geração Mint 25/08, projeto CS BRASIL - Time Mítico; prompt próprio (text-to-3d, sem referência de terceiro). Otimizado no padrão gen-asset (dedup + textureCompress webp 1024 + prune). Estático; nado procedural (serpenteio submerso) no ambientlife.js." + }, + "processing": { + "provider": "mint", + "model": "mint text-to-3d (original)", + "tools": [ + "mint.gg", + "padrão tools/gen-asset.mjs (dedup/textureCompress webp 1024/prune)" + ], + "finalSha256": "c84cc4ff23c4da8483030812b28127dc831c1992ea0fa61ffabd676f38fe23cb" + } + }, + "onca-pintada-amazonia": { + "artifactType": "model/gltf-binary", + "files": [ + "public/models/ambient/onca_pintada.glb" + ], + "source": { + "kind": "mint-chat", + "assetId": "ks7a7gfwpmeze4kypdk7p75grn8d5v1e", + "chatUrl": "https://mint.gg/chat/ph7fw0nnwd3r0t3sd3xjd5m0yn8d5y56", + "displayName": "Golden Rosetta Jaguar (onça-pintada deitada)", + "notes": "Fauna viva do mapa amazonia (PR #439): onça-pintada deitada em tronco à margem, respiração por escala sutil. Geração Mint 25/08, projeto CS BRASIL - Time Mítico; prompt próprio. Otimizada no padrão gen-asset (dedup + webp 1024 + prune). Estática; procedural no ambientlife.js." + }, + "processing": { + "provider": "mint", + "model": "mint text-to-3d (original)", + "tools": [ + "mint.gg", + "padrão tools/gen-asset.mjs (dedup/textureCompress webp 1024/prune)" + ], + "finalSha256": "a210448f00b82f7e1e8f4a13d46b0fb2c17cfc4b0a30e0eff449b308b72c12fe" + } + }, + "arara-vermelha-amazonia": { + "artifactType": "model/gltf-binary", + "files": [ + "public/models/ambient/arara_vermelha.glb" + ], + "source": { + "kind": "mint-chat", + "assetId": "ks7dzf3k95me5hf288kdra4bhh8d5pe3", + "chatUrl": "https://mint.gg/chat/ph74hq1an9ymzcatgm90vb78zx8d4apw", + "displayName": "Scarlet Wing Macaw (arara-vermelha de poleiro)", + "notes": "Fauna viva do mapa amazonia (PR #439): arara-vermelha empoleirada nos postes do market flutuante, balanço procedural. Geração Mint 25/08, projeto CS BRASIL - Time Mítico; prompt próprio. Otimizada no padrão gen-asset (dedup + webp 1024 + prune). Estática; procedural no ambientlife.js." + }, + "processing": { + "provider": "mint", + "model": "mint text-to-3d (original)", + "tools": [ + "mint.gg", + "padrão tools/gen-asset.mjs (dedup/textureCompress webp 1024/prune)" + ], + "finalSha256": "8fa0febb139a27f62ae0a3704cbf73a8f296ccbcc560c62b1ab4a166d126a20d" + } + }, + "tucano-amazonia": { + "artifactType": "model/gltf-binary", + "files": [ + "public/models/ambient/tucano.glb" + ], + "source": { + "kind": "mint-chat", + "assetId": "ks78kjkm9v47b1r31r4n7ybxkh8d4sky", + "chatUrl": "https://mint.gg/chat/ph7dqhakak88w9tnyc5z2r5r0x8d4mre", + "displayName": "Green Beak Toucan (tucano de poleiro)", + "notes": "Fauna viva do mapa amazonia (PR #439): tucano-de-bico-verde empoleirado nas caixas d'água das palafitas, balanço procedural. Geração Mint 25/08, projeto CS BRASIL - Time Mítico; prompt próprio. Otimizado no padrão gen-asset (dedup + webp 1024 + prune). Estático; procedural no ambientlife.js." + }, + "processing": { + "provider": "mint", + "model": "mint text-to-3d (original)", + "tools": [ + "mint.gg", + "padrão tools/gen-asset.mjs (dedup/textureCompress webp 1024/prune)" + ], + "finalSha256": "4c5f0b78a48cd160ef9786d688d764543baa8d897ce59c0fcc360f08a7476d22" + } + }, + "preguica-amazonia": { + "artifactType": "model/gltf-binary", + "files": [ + "public/models/ambient/preguica.glb" + ], + "source": { + "kind": "mint-chat", + "assetId": "ks78j2sc4461g9nkfz43mmn1rd8d48wr", + "chatUrl": "https://mint.gg/chat/ph781sy8yevx09m0kxgcxefxvx8d5n9j", + "displayName": "Algae Sloth Pendant (preguiça pendurada)", + "notes": "Fauna viva do mapa amazonia (PR #439): preguiça pendurada no beiral da palafita, quase parada. Geração Mint 25/08, projeto CS BRASIL - Time Mítico; prompt próprio. Otimizada no padrão gen-asset (dedup + webp 1024 + prune). Estática; procedural no ambientlife.js." + }, + "processing": { + "provider": "mint", + "model": "mint text-to-3d (original)", + "tools": [ + "mint.gg", + "padrão tools/gen-asset.mjs (dedup/textureCompress webp 1024/prune)" + ], + "finalSha256": "de88a5bfe1e48a132dca9d3f985b7ef0b55133c5e1253bf8f2020d1ceeb09c7d" + } + }, + "macaco-prego-amazonia": { + "artifactType": "model/gltf-binary", + "files": [ + "public/models/ambient/macaco_prego.glb" + ], + "source": { + "kind": "mint-chat", + "assetId": "ks793sf1tpkvt3pxq7jjhvpam98d5aa9", + "chatUrl": "https://mint.gg/chat/ph76c29s8yyr4sy8cqxe36t9v98d4zv5", + "displayName": "Golden Tuft Capuchin (macaco-prego saltador)", + "notes": "Fauna viva do mapa amazonia (PR #439): macaco-prego pulando entre telhado da madeireira e bancadas. Geração Mint 25/08, projeto CS BRASIL - Time Mítico; prompt próprio. Otimizado no padrão gen-asset (dedup + webp 1024 + prune). Estático; salto procedural no ambientlife.js." + }, + "processing": { + "provider": "mint", + "model": "mint text-to-3d (original)", + "tools": [ + "mint.gg", + "padrão tools/gen-asset.mjs (dedup/textureCompress webp 1024/prune)" + ], + "finalSha256": "66d4e4423ddf62acc7e22c4c2c399c2f18e008d2bfb83369c1aeadc37f76fd9a" + } + }, + "anta-amazonia": { + "artifactType": "model/gltf-binary", + "files": [ + "public/models/ambient/anta.glb" + ], + "source": { + "kind": "mint-chat", + "assetId": "ks7ecr51smg0h2f8nsv1h1rdjh8d5frt", + "chatUrl": "https://mint.gg/chat/ph7a0wexywqytnxkrjy60bvqd98d47s5", + "displayName": "Lowland Tapir Standing (anta da borda da mata)", + "notes": "Fauna viva do mapa amazonia (PR #439): anta pastejando na borda da mata, locomoção procedural do idioma quad. Geração Mint 25/08, projeto CS BRASIL - Time Mítico; prompt próprio. Otimizada no padrão gen-asset (dedup + webp 1024 + prune). Estática." + }, + "processing": { + "provider": "mint", + "model": "mint text-to-3d (original)", + "tools": [ + "mint.gg", + "padrão tools/gen-asset.mjs (dedup/textureCompress webp 1024/prune)" + ], + "finalSha256": "5a56336945d1d7db691cd521e149e7c1ae4d6f38a36d5bbe404d62248e5d25c8" + } + }, + "piranha-amazonia": { + "artifactType": "model/gltf-binary", + "files": [ + "public/models/ambient/piranha.glb" + ], + "source": { + "kind": "mint-chat", + "assetId": "ks70d9arxnzj5y7yvtt8wmnw2d8d5nmv", + "chatUrl": "https://mint.gg/chat/ph78k1ydatb41te477m5yf3wq18d5jns", + "displayName": "Silver Fang Piranha (piranha do cardume)", + "notes": "Fauna viva do mapa amazonia (PR #439): piranha prateada em cardume instanciado (rota circular) no igarapé. Geração Mint 25/08, projeto CS BRASIL - Time Mítico; prompt próprio. Otimizada no padrão gen-asset (dedup + webp 1024 + prune). Estática; cardume procedural no ambientlife.js." + }, + "processing": { + "provider": "mint", + "model": "mint text-to-3d (original)", + "tools": [ + "mint.gg", + "padrão tools/gen-asset.mjs (dedup/textureCompress webp 1024/prune)" + ], + "finalSha256": "32459720fe88a477eb1e26a67655b87534148867a67368816cf02a7d196d6581" + } + }, + "carcara-amazonia": { + "artifactType": "model/gltf-binary", + "files": [ + "public/models/ambient/carcara.glb" + ], + "source": { + "kind": "mint-chat", + "assetId": "ks70zqfdxbtnvqxpjr78s61wb58d45av", + "chatUrl": "https://mint.gg/chat/ph74edcs8540fz6m7dbn8v2bdd8d589h", + "displayName": "Striped Breast Caracara (carcará da serragem)", + "notes": "Fauna viva do mapa amazonia (PR #439): carcará no chão da madeireira com passo ocasional. Geração Mint 25/08, projeto CS BRASIL - Time Mítico; prompt próprio. Otimizado no padrão gen-asset (dedup + webp 1024 + prune). Estático; procedural no ambientlife.js." + }, + "processing": { + "provider": "mint", + "model": "mint text-to-3d (original)", + "tools": [ + "mint.gg", + "padrão tools/gen-asset.mjs (dedup/textureCompress webp 1024/prune)" + ], + "finalSha256": "0b366c74f4a3df22bf063ae66ba965092bcf11235914daf19a67fdae0350c3af" + } + }, "mansao_jardim_pack": { "files": [ "public/models/props/banco_jardim.glb", diff --git a/package.json b/package.json index 7d705e10..f8352d49 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,8 @@ "eval:parquewheel": "node tools/eval/parque-wheel-check.mjs", "eval:velhooeste": "node tools/eval/velho-oeste-check.mjs", "eval:penitenciaria": "node tools/eval/penitenciaria-check.mjs", + "//eval:amazonia": "frente map2/amazonia: o 'não parece low poly' do PR #375 virou número — variedade de material, densidade de cover, água viva e fauna (mutantes provam cada cláusula)", + "eval:amazonia": "node tools/eval/amazonia-check.mjs", "eval:vminspect": "node tools/eval/vm-inspect-check.mjs", "eval:pegada": "node tools/eval/pegada-check.mjs", "eval:dmgdir": "node tools/eval/dmgdir-check.mjs", diff --git a/public/img/FONTE.md b/public/img/FONTE.md index 7a581dd1..ac228f18 100644 --- a/public/img/FONTE.md +++ b/public/img/FONTE.md @@ -395,3 +395,45 @@ gerador; a entrega é WebP `1774×887`. Nenhum prompt histórico foi reconstruí translucent smoke texture, no text, no other objects". Sem canal alfa — a luminância vira alfa no shader (uLumAlpha do gpuparticles.js). SHA-256 `249621d02702caee19f5cf85c14e1e77c15b92dea56148e2361f7389f311057a`. + + +- `textures/sky_amazonia.webp`: panorama equiretangular 2:1 do céu equatorial úmido + (1774×887). Prompt: "Create a seamless equirectangular 2:1 environment panorama + for a stylized-realistic browser FPS set in the Brazilian Amazon. Upper 60 percent + is a humid equatorial sky: soft diffused light, layered low cumulus clouds, moist + pale greenish-white haze near the horizon, muted blue-grey zenith. Lower 40 percent + horizon shows endless flat rainforest canopy of dark green rounded treetops and a + thin dark river winding through, all fading into dense water-green atmospheric + haze. Overcast humid morning light, no identifiable landmark, no buildings, no + people, no text, no watermark, no borders. Output only the panorama." + SHA-256 `b7f21ceffb65d139e8efd79b4ff2e172d7b83358f5b41291427dcc74569eaa1e`. +- `textures/tex_palha.webp` (512×512): palha de telhado de palafita. Prompt: + "Top-down seamless texture of weathered dried thatch straw, Amazon palm thatch + roof material seen from above, warm ochre tan straw strands laid in parallel + directional rows, some strands darker and damp, natural irregularities, even + ambient light, photorealistic, no people, no text, no watermark". SHA-256 + `2d8edcd6b03cdd5995f3f65a8c2e513ddb97729d61d947d929a8e6b754e77fca`. +- `textures/tex_selva.webp` (512×512): chão de mata. Prompt: "Top-down seamless + texture of Amazon rainforest floor, dark wet soil with fallen leaves, broken + twigs, moss patches, few roots, damp organic litter, muted browns and deep + greens, even ambient light, photorealistic, no people, no text, no watermark". + SHA-256 `e51e8050fce458857ee6094863d1bbce413a7dc04bd29cf4676f81989bf2f6a9`. +- `textures/tex_madeira_serragem.webp` (512×512): piso da madeireira. Prompt: + "Top-down seamless texture of abandoned sawmill dirt floor, old grey weathered + wooden planks with sawdust, scattered wood chips and sawdust piles, rusty + stains, oil marks, worn industrial ground, even ambient light, photorealistic, + no people, no text, no watermark". SHA-256 + `c4bd285b03d27ec1b9469dd7ebd3020961a5bb44e24f49a8b6791beb1c8622ba`. +- `map-previews/amazonia.jpg` (640×640): cartaz do menu (provisório até captura + 3:2 do mapa real pela bateria única de browser). Prompt: "Stylized-realistic + game map preview, Brazilian Amazon riverside community: a wide igarape river + with dark green water crossing the middle, stilt wooden houses (palafitas) with + palm thatch roofs on both banks, a floating wooden market with colorful canoes + and tarps, an abandoned sawmill shed in the background, dense jungle wall and + tall ceiba trees behind, humid hazy green sky, no people, no text, no + watermark". SHA-256 + `7f0a1c3db390e1c30c80434d42a10ba11c3786729773b65e6e69a5ca24fc5efa`. diff --git a/public/img/map-previews/amazonia.jpg b/public/img/map-previews/amazonia.jpg new file mode 100644 index 00000000..43a676fa Binary files /dev/null and b/public/img/map-previews/amazonia.jpg differ diff --git a/public/img/textures/sky_amazonia.webp b/public/img/textures/sky_amazonia.webp new file mode 100644 index 00000000..222369f8 Binary files /dev/null and b/public/img/textures/sky_amazonia.webp differ diff --git a/public/img/textures/tex_madeira_serragem.webp b/public/img/textures/tex_madeira_serragem.webp new file mode 100644 index 00000000..01febc9a Binary files /dev/null and b/public/img/textures/tex_madeira_serragem.webp differ diff --git a/public/img/textures/tex_palha.webp b/public/img/textures/tex_palha.webp new file mode 100644 index 00000000..1024cd04 Binary files /dev/null and b/public/img/textures/tex_palha.webp differ diff --git a/public/img/textures/tex_selva.webp b/public/img/textures/tex_selva.webp new file mode 100644 index 00000000..17aef6c2 Binary files /dev/null and b/public/img/textures/tex_selva.webp differ diff --git a/public/js/ambientlife.js b/public/js/ambientlife.js index 0cccef5d..1e856a62 100644 --- a/public/js/ambientlife.js +++ b/public/js/ambientlife.js @@ -19,18 +19,48 @@ const ASSETS = Object.freeze({ armadillo: 'models/ambient/tatu_campo.glb', cockroach: 'models/ambient/barata_urbana.glb', parrot: 'models/ambient/papagaio_poleiro.glb', + /* map2/amazonia (PR #439, geração Mint 25/08 "CS BRASIL - Time Mítico"): + 9 espécies estáticas — a vida é procedural aqui embaixo. Só entram no + preload de quem as usa (AMAZONIA_FAUNA_ASSETS), não no default. */ + boto: 'models/ambient/boto_amazonia.glb', + onca: 'models/ambient/onca_pintada.glb', + tucano: 'models/ambient/tucano.glb', + preguica: 'models/ambient/preguica.glb', + macaco: 'models/ambient/macaco_prego.glb', + anta: 'models/ambient/anta.glb', + arara: 'models/ambient/arara_vermelha.glb', + piranha: 'models/ambient/piranha.glb', + carcara: 'models/ambient/carcara.glb', }); -export const FAVELA_AMBIENCE_ASSETS = Object.freeze(Object.keys(ASSETS)); -const TYPE_ASSET = Object.freeze({ rat: 'rat', pigeon: 'pigeonGround', dog: 'dog', cat: 'cat', chicken: 'chicken', cow: 'cow', armadillo: 'armadillo', cockroach: 'cockroach', parrot: 'parrot' }); -const FAUNA_NAME = Object.freeze({ rat: 'rato', pigeon: 'pomba', dog: 'cachorro', cat: 'gato', chicken: 'galinha', cow: 'vaca', armadillo: 'tatu', cockroach: 'barata', parrot: 'papagaio' }); +/* FAVELA_AMBIENCE_ASSETS era `Object.keys(ASSETS)`; virou lista explícita quando + ASSETS passou a abrigar também a fauna da amazonia: mapa sem bicho não baixa + bicho (BUG-57) — keys(ASSETS) poria 3 MiB extras no preload de todo mapa. */ +export const FAVELA_AMBIENCE_ASSETS = Object.freeze(['rat', 'pigeonGround', 'dog', 'jacare', 'capivara', 'cat', 'chicken', 'cow', 'armadillo', 'cockroach', 'parrot']); +export const AMAZONIA_FAUNA_ASSETS = Object.freeze(['boto', 'onca', 'tucano', 'preguica', 'macaco', 'anta', 'arara', 'piranha', 'carcara']); +const TYPE_ASSET = Object.freeze({ rat: 'rat', pigeon: 'pigeonGround', dog: 'dog', cat: 'cat', chicken: 'chicken', cow: 'cow', armadillo: 'armadillo', cockroach: 'cockroach', parrot: 'parrot', boto: 'boto', onca: 'onca', tucano: 'tucano', preguica: 'preguica', macaco: 'macaco', anta: 'anta', arara: 'arara', piranha: 'piranha', carcara: 'carcara' }); +const FAUNA_NAME = Object.freeze({ rat: 'rato', pigeon: 'pomba', dog: 'cachorro', cat: 'gato', chicken: 'galinha', cow: 'vaca', armadillo: 'tatu', cockroach: 'barata', parrot: 'papagaio', boto: 'boto', onca: 'onça-pintada', tucano: 'tucano', preguica: 'preguiça', macaco: 'macaco-prego', anta: 'anta', arara: 'arara-vermelha', piranha: 'piranha', carcara: 'carcará' }); const QUADS = new Set(['dog', 'cat', 'chicken', 'cow', 'armadillo']); const SHOT_REACTION_RADIUS = 13; const DOG_IDLE_TIME = 3; /* por tipo: duração do susto e velocidade de fuga/caminhada (vaca larga, gato rápido) */ -const ALERT_TIME = Object.freeze({ rat: 2.1, dog: 2.6, cat: 2.4, chicken: 2.8, cow: 3.2, pigeon: 3.2, armadillo: 2.4, cockroach: 1.8, parrot: 1.3 }); +const ALERT_TIME = Object.freeze({ rat: 2.1, dog: 2.6, cat: 2.4, chicken: 2.8, cow: 3.2, pigeon: 3.2, armadillo: 2.4, cockroach: 1.8, parrot: 1.3, boto: 1.5, onca: 2.6, tucano: 1.3, preguica: 4, macaco: 1.6, anta: 2.8, arara: 1.4, piranha: 1.2, carcara: 1.5 }); const QUAD_SPEED = Object.freeze({ dog: { walk: 1, flee: 3.2 }, cat: { walk: 1.1, flee: 3.6 }, chicken: { walk: .55, flee: 2.6 }, cow: { walk: .75, flee: 2.4 }, armadillo: { walk: .4, flee: 1.5 }, // tatu é bicho de passo curto; fuga é um trote rápido + anta: { walk: .55, flee: 3 }, carcara: { walk: .35, flee: 2.2 }, +}); +/* map2/amazonia: len/yawFix das 9 espécies Mint (cabeça→−Z), derivados geometricamente + e calibrados contra jacare=π/2/capivara=0 — verificação visual 3:2 pendente (PR #439). */ +const AMAZONIA_FAUNA_META = Object.freeze({ + boto: { len: 2.0, yawFix: Math.PI / 2 }, + onca: { len: 1.5, yawFix: Math.PI / 2 }, + tucano: { len: .42, yawFix: Math.PI / 2 }, + preguica: { len: .65, yawFix: Math.PI / 2 }, + macaco: { len: .42, yawFix: Math.PI }, + anta: { len: 1.8, yawFix: Math.PI / 2 }, + arara: { len: .8, yawFix: Math.PI }, + piranha: { len: .32, yawFix: Math.PI / 2 }, + carcara: { len: .5, yawFix: Math.PI / 2 }, }); const loadGLB = (url) => new Promise((resolve, reject) => loader.load(url, resolve, undefined, reject)); @@ -176,8 +206,10 @@ function normalizeModel(id, model) { const size = box.getSize(new THREE.Vector3()); /* alvo em metros de mundo: altura para bichos que andam de lado pro jogador, comprimento para rato (silhueta deitada). Vaca 1,75 / gato 0,48 / galinha 0,5. */ - const target = { rat: .36, pigeonGround: .29, dog: 1, cat: .48, chicken: .5, cow: 1.75, armadillo: .55, cockroach: .14, parrot: .34 }[id] || .5; - const dimension = ['rat', 'armadillo', 'cockroach'].includes(id) ? Math.max(size.x, size.z) : size.y; + const target = { rat: .36, pigeonGround: .29, dog: 1, cat: .48, chicken: .5, cow: 1.75, armadillo: .55, cockroach: .14, parrot: .34, ...Object.fromEntries(AMAZONIA_FAUNA_ASSETS.map((id) => [id, AMAZONIA_FAUNA_META[id].len])) }[id] || .5; + // amazonia: bicho deitado (boto/onça/anta/piranha/preguiça) normaliza por comprimento + const HORIZONTAIS = new Set(['rat', 'armadillo', 'cockroach', 'boto', 'onca', 'anta', 'piranha', 'preguica']); + const dimension = HORIZONTAIS.has(id) ? Math.max(size.x, size.z) : size.y; const scale = target / Math.max(.001, dimension); // dog: altura 1 m => cernelha ~0,6 (ombro 1,83 de 3,09 de altura no GLB bruto) const center = box.getCenter(new THREE.Vector3()); @@ -197,7 +229,7 @@ function distanceToSegment(point, start, end) { } class FavelaAmbience { - constructor(root, { map, low = false, rats = [], pigeons = [], dogs = [], cats = [], chickens = [], cows = [], armadillos = [], cockroaches = [], parrots = [] }) { + constructor(root, { map, low = false, rats = [], pigeons = [], dogs = [], cats = [], chickens = [], cows = [], armadillos = [], cockroaches = [], parrots = [], botos = [], oncas = [], tucanos = [], preguicas = [], macacos = [], antas = [], araras = [], piranhas = [], carcaras = [] }) { this.map = map; this.low = low; this.time = 0; @@ -216,6 +248,15 @@ class FavelaAmbience { const armadilloList = low ? armadillos.slice(0, 1) : armadillos; const cockroachList = low ? cockroaches.slice(0, 1) : cockroaches; const parrotList = low ? parrots.slice(0, 1) : parrots; + const botoList = low ? botos.slice(0, 1) : botos; + const oncaList = low ? oncas.slice(0, 1) : oncas; + const tucanoList = low ? tucanos.slice(0, 1) : tucanos; + const preguicaList = low ? preguicas.slice(0, 1) : preguicas; + const macacoList = low ? macacos.slice(0, 1) : macacos; + const antaList = low ? antas.slice(0, 1) : antas; + const araraList = low ? araras.slice(0, 1) : araras; + const piranhaList = low ? piranhas.slice(0, 1) : piranhas; + const carcaraList = low ? carcaras.slice(0, 1) : carcaras; this.animals = []; ratList.forEach((config, index) => this._add('rat', config, index)); pigeonList.forEach((config, index) => this._add('pigeon', config, index)); @@ -226,6 +267,15 @@ class FavelaAmbience { armadilloList.forEach((config, index) => this._add('armadillo', config, index)); cockroachList.forEach((config, index) => this._add('cockroach', config, index)); parrotList.forEach((config, index) => this._add('parrot', config, index)); + botoList.forEach((config, index) => this._add('boto', config, index)); + oncaList.forEach((config, index) => this._add('onca', config, index)); + tucanoList.forEach((config, index) => this._add('tucano', config, index)); + preguicaList.forEach((config, index) => this._add('preguica', config, index)); + macacoList.forEach((config, index) => this._add('macaco', config, index)); + antaList.forEach((config, index) => this._add('anta', config, index)); + araraList.forEach((config, index) => this._add('arara', config, index)); + piranhaList.forEach((config, index) => this._add('piranha', config, index)); + carcaraList.forEach((config, index) => this._add('carcara', config, index)); this.reset(); } @@ -350,7 +400,12 @@ class FavelaAmbience { } if (animal.type === 'rat' || animal.type === 'cockroach') this._updateRat(animal, dt); else if (animal.type === 'pigeon') this._updatePigeon(animal, dt); - else if (animal.type === 'parrot') this._updateParrot(animal, dt); + else if (animal.type === 'parrot' || animal.type === 'arara' || animal.type === 'tucano') this._updateParrot(animal, dt); + else if (animal.type === 'boto') this._updateBoto(animal); + else if (animal.type === 'piranha') this._updatePiranha(animal, dt); + else if (animal.type === 'preguica') this._updatePreguica(animal); + else if (animal.type === 'macaco') this._updateMacaco(animal); + else if (animal.type === 'onca') this._updateOnca(animal); else this._updateQuad(animal, dt); animal.mixer?.update(dt); } @@ -471,6 +526,95 @@ class FavelaAmbience { return false; } + /* ── VIDA PROCEDURAL DA AMAZONIA (PR #439): Mint entrega estático, o + comportamento mora aqui. Yaw de locomoção = atan2(dir) + yawFix + π + (yawFix aponta a cabeça pra −Z, o atan2 trabalha com +Z de frente). ── */ + + _noseYaw(animal, dirX, dirZ) { + return Math.atan2(dirX, dirZ) + (AMAZONIA_FAUNA_META[animal.type]?.yawFix || 0) + Math.PI; + } + + _updateBoto(animal) { + /* boto NADA no igarapé: vai-e-vem lento no canal serpenteando em x; o + dorso fura a lâmina no topo da oscilação e afunda de novo. */ + const t = this.time + animal.phase * 7; + const tri = Math.abs(((t / 46) % 2) - 1); // 0→1→0 em 92 s + const zz = THREE.MathUtils.lerp(animal.origin.z, animal.to.z, tri); + const dirZ = animal.to.z > animal.origin.z ? 1 : -1; + const serp = Math.sin(t * .32 + animal.phase * 2); + const xx = animal.origin.x + serp * 2.2; + const yy = animal.origin.y + Math.sin(t * .11 + animal.phase) * .2; + animal.root.position.set(xx, yy, zz); + const dx = Math.cos(t * .32 + animal.phase * 2) * 2.2 * .32; + const dz = (this.time < animal.alertUntil ? 1.6 : 1) * (tri < .5 ? dirZ : -dirZ); + animal.root.rotation.y = this._noseYaw(animal, dx, dz * .4); + animal.root.rotation.x = -Math.cos(t * .11 + animal.phase) * .12; + animal.state = this.time < animal.alertUntil ? 'flee' : 'swim'; + } + + _updatePiranha(animal, dt) { + /* CARDUME: cada piranha é um animal do mesmo cardume — centro comum + (origin), círculo com raio/fase próprios e mesma rotação; tiro perto + aperta o raio (meio metro) e solta devagar. */ + const t = this.time + animal.phase * 3; + const alerta = this.time < animal.alertUntil; + const raioAlvo = alerta ? animal.radius[0] * .35 : animal.radius[0]; + animal.rCur = animal.rCur === undefined ? animal.radius[0] : animal.rCur + (raioAlvo - animal.rCur) * Math.min(1, dt * 1.5); + const ang = t * (alerta ? 1.5 : .5); + const xx = animal.origin.x + Math.cos(ang) * animal.rCur; + const zz = animal.origin.z + Math.sin(ang) * animal.rCur; + animal.root.position.set(xx, animal.origin.y + Math.sin(t * 2.3 + animal.phase) * .04, zz); + animal.root.rotation.y = this._noseYaw(animal, -Math.sin(ang), Math.cos(ang)); + animal.state = alerta ? 'flee' : 'school'; + } + + _updatePreguica(animal) { + /* pendurada no beiral: flip de Z (modelo Mint vem em pé), pêndulo + lentíssimo — preguiça não foge de tiro, é a piada do bicho. */ + const t = this.time + animal.phase * 11; + animal.root.rotation.z = Math.PI + Math.sin(t * .25) * .04; + animal.root.rotation.x = Math.sin(t * .11 + 1) * .02; + animal.root.rotation.y = animal.phase + (AMAZONIA_FAUNA_META.preguica.yawFix || 0) + Math.PI + Math.sin(t * .05) * .2; + animal.state = 'idle'; + } + + _updateMacaco(animal) { + /* macaco-prego PULA entre dois pontos (config pos/to): idle olhando o + alvo, salto parabólico; tiro perto antecipa o salto. */ + const t = this.time; + const mid = animal.origin.clone().add(animal.to).multiplyScalar(.5); + if (animal.jumpAt === undefined) { + const querSaltar = (t + animal.phase * 5) % 3.5 < .08 && t > (animal.idleUntil || 0); + if (this.time < animal.alertUntil || querSaltar) { + animal.jumpAt = t; + animal.jumpFrom = animal.root.position.clone(); + animal.jumpTo = (animal.hop ? animal.origin : animal.to).clone(); + } else { + const alvo = (animal.hop ? animal.origin : animal.to).clone().sub(mid).setY(0); + animal.root.rotation.y += (this._noseYaw(animal, alvo.x, alvo.z) - animal.root.rotation.y) * .08; + animal.state = 'idle'; + } + } + if (animal.jumpAt !== undefined) { + const u = Math.min(1, (t - animal.jumpAt) / .55); + animal.root.position.lerpVectors(animal.jumpFrom, animal.jumpTo, u); + animal.root.position.y += Math.sin(u * Math.PI) * 1.35; + const dir = animal.jumpTo.clone().sub(animal.jumpFrom); + animal.root.rotation.y = this._noseYaw(animal, dir.x, dir.z); + animal.state = 'jump'; + if (u >= 1) { animal.hop = !animal.hop; animal.jumpAt = undefined; animal.idleUntil = t + 2.6; } + } + } + + _updateOnca(animal) { + /* onça DEITADA no tronco: não anda — só respira (escala sutil no root, + a base fica no lugar porque o pivô está nos pés). */ + const breathe = 1 + Math.sin(this.time * 1.9 + animal.phase) * .012; + animal.root.scale.setScalar(breathe); + animal.root.rotation.y = animal.phase + (AMAZONIA_FAUNA_META.onca.yawFix || 0) + Math.PI; + animal.state = 'idle'; + } + snapshot() { return this.animals.map((animal) => ({ id: animal.id, type: animal.type, state: animal.state, @@ -496,9 +640,12 @@ class FavelaAmbience { const armadillo = this.animals.filter((animal) => animal.type === 'armadillo').length; const cockroach = this.animals.filter((animal) => animal.type === 'cockroach').length; const parrot = this.animals.filter((animal) => animal.type === 'parrot').length; + const porTipo = {}; + for (const animal of this.animals) porTipo[animal.type] = (porTipo[animal.type] || 0) + 1; + const amazonia = Object.fromEntries(AMAZONIA_FAUNA_ASSETS.map((id) => [id, porTipo[id] || 0])); return { map: this.map, low: this.low, gltf: this.animals.length > 0 && this.animals.every((animal) => animal.source === 'gltf'), - counts: { rat, pigeon, dog, cat, chicken, cow, armadillo, cockroach, parrot, total: rat + pigeon + dog + cat + chicken + cow + armadillo + cockroach + parrot }, meshes, triangles: Math.round(triangles), + counts: { rat, pigeon, dog, cat, chicken, cow, armadillo, cockroach, parrot, total: this.animals.length, ...amazonia }, meshes, triangles: Math.round(triangles), }; } diff --git a/public/js/look.js b/public/js/look.js index e5562a14..e95c1ec7 100644 --- a/public/js/look.js +++ b/public/js/look.js @@ -28,4 +28,13 @@ export const LOOK = { neblina: { d: 0.0082, solDir: [28, 38, 18], forca: 0.84 }, grade: { exposicao: 1.60, piso: 0.0048, expAces: 1.70 }, }, + amazonia: { // equador úmido: céu lavado de água, névoa verde densa + sky: '/img/textures/sky_amazonia.webp', + horizonte: 0xc9d0cd, // look-horizonte.py sobre sky_amazonia.webp (banda 429-441 de 887) + zenite: 0x7c919f, + sol: { cor: 0xfff2dc, i: 1.45, pos: [12, 38, 10] }, + hemi: { ceu: 0xdfe8e4, chao: 0x3d4a33, i: 1.05 }, + neblina: { d: 0.0112, solDir: [12, 38, 10], forca: 0.92 }, + grade: { exposicao: 1.50, piso: 0.0050, expAces: 1.60 }, + }, }; diff --git a/public/js/map_amazonia.js b/public/js/map_amazonia.js new file mode 100644 index 00000000..1da9096a --- /dev/null +++ b/public/js/map_amazonia.js @@ -0,0 +1,501 @@ +// AMAZONIA (amazonia) — retrato do "Treta no Vietnã" (PR #375) como comunidade +// ribeirinha: igarapé, palafitas de palha, market flutuante, madeireira. Spec: ~/map2/prompt-opencode.md. +import * as THREE from 'three'; +import { placeProp, hasProp } from './mapprops.js'; +import { applyLook } from './map_sky.js'; +import { createWater } from './water.js'; +import { createFavelaAmbience, placeFauna, CORREGO_FAUNA_ASSETS, AMAZONIA_FAUNA_ASSETS } from './ambientlife.js'; +import { AMB_LOOPS } from './soundscape.js'; +import { detailFor } from './textures.js'; + +const QP = new URLSearchParams(typeof location !== 'undefined' ? location.search : ''); +const LOWQ = (() => { try { return JSON.parse(localStorage.getItem('awpbr_settings') || '{}').quality === 'low'; } catch (e) { return false; } })(); + +export const HALF_X = 32, HALF_Z = 44; +export const AMAZONIA_AMBIENCE = Object.freeze([...CORREGO_FAUNA_ASSETS, ...AMAZONIA_FAUNA_ASSETS]); // + jacaré/capivara e o elenco novo (este mapa baixa) + +/* ── IGARAPÉ: rio em x ∈ [−9,75, 9,75]; centro fundo (−0,6), margens em rampa até −0,28 — + quem cai da ponte vadeia e sai pela margem (lição das rampas do córrego). */ +const RIO_MEIA_LARGURA = 9.75; +const RIO_FUNDO = -0.6, RIO_MARGEM = -0.28, RIO_AGUA = 0.20; +const RIO_CAMPO = 6.5; // além daqui a rampa começa +const PONTE_W = 3.4, PONTE_Y = 0.18; + +export const AMAZONIA_PROPS = ['samambaia', 'heliconia', 'planta_corrego_taboa', + 'planta_corrego_taioba', 'grama_corrego_01', 'grama_corrego_02', 'stall', 'arara_roupas', + 'caixa_dagua', 'botijao_gas', 'pilha_pneus', 'tires', 'dumpster']; + +/* Palafitas: (x, z, ry). Espalhadas pelas duas margens para o MAP5 não ter quadrante + deserto — os esteios finos são a densidade de cover barata e legível do mapa. */ +const PALAFITAS = [ + { x: 14, z: -26, ry: 0.21, cor: 0xc4a577 }, { x: 21, z: -18, ry: -0.14, cor: 0xb39468 }, + { x: 27, z: -7, ry: 0.09, cor: 0xcdaf7f }, { x: 15.5, z: -9, ry: 0.28, cor: 0xbd9d6d }, + { x: 14, z: 9, ry: -0.19, cor: 0xc9a97a }, { x: 21, z: 17, ry: 0.15, cor: 0xb6975f }, + { x: 27, z: 7, ry: -0.26, cor: 0xc0a06c }, { x: 15.5, z: 26, ry: 0.11, cor: 0xccae7d }, + { x: -14, z: -22, ry: 0.18, cor: 0xb99a66 }, { x: -15, z: 20, ry: -0.16, cor: 0xc7a878 }, + { x: -27, z: -25, ry: 0.24, cor: 0xbf9f6a }, +]; +const ESTACA_H = 2.5, DECK_E = 0.4; // dequee em ESTACA_H, espessura DECK_E +/* Booms de lenha boiando: cada tronco é colisor de verdade — a travessia de barco + do igarapé é cover no meio da água (MAP5 cobra o quadrante do rio também). */ +const BOOMS = [ + { z: -26, x0: -9.4, n: 8 }, { z: -14, x0: -8.6, n: 6 }, { z: -33, x0: 2.2, n: 6 }, + { z: 12, x0: -8.6, n: 6 }, { z: 26, x0: -9.4, n: 8 }, { z: 33, x0: 2.2, n: 6 }, +]; + +export function buildAmazonia(scene, T) { + const colliders = [], occluders = [], pickups = []; + const root = new THREE.Group(); + root.name = 'treta-na-amazonia'; + scene.add(root); + + const lam = (o) => { + const m = new THREE.MeshStandardMaterial({ roughness: 0.95, metalness: 0, ...o }); + const det = m.map && detailFor(m.map); + if (det) { + if (det.normalMap && !m.normalMap) { m.normalMap = det.normalMap; m.normalScale.set(0.65, 0.65); } + if (det.roughnessMap && !m.roughnessMap) m.roughnessMap = det.roughnessMap; + } + return m; + }; + + let TEX = { + selva: lam({ map: T.dirt, roughness: 1 }), madeira: lam({ map: T.dirt }), + serragem: lam({ map: T.dirt }), palha: lam({ map: T.dirt }), + agua: lam({ map: T.dirt, color: 0x42543b, roughness: 0.24, metalness: 0.12 }), + }; + if (typeof document !== 'undefined') { + const load = (url, rx = 4, ry = 4) => { + const t = new THREE.TextureLoader().load(url); + t.colorSpace = THREE.SRGBColorSpace; + t.wrapS = t.wrapT = THREE.RepeatWrapping; t.repeat.set(rx, ry); + return t; + }; + TEX.selva = lam({ map: load('/img/textures/tex_selva.webp', 10, 14), roughness: 1 }); + TEX.madeira = lam({ map: load('/img/textures/tex_madeira.webp', 2, 2) }); + TEX.serragem = lam({ map: load('/img/textures/tex_madeira_serragem.webp', 5, 3), roughness: 1 }); + TEX.palha = lam({ map: load('/img/textures/tex_palha.webp', 3, 2), roughness: 0.98 }); + TEX.agua = lam({ map: load('/img/textures/tex_agua_poluida.webp', 2, 6), color: 0x42543b, roughness: 0.24, metalness: 0.12 }); + } + const texDe = (src, rx, ry) => { + if (!src.map) return src.map; + const t = src.map.clone(); t.needsUpdate = true; + t.wrapS = t.wrapT = THREE.RepeatWrapping; t.repeat.set(rx, ry); + return t; + }; + const matSelva = lam({ map: texDe(TEX.selva, 10, 14) || T.dirt, roughness: 1 }); + const matFundo = lam({ map: texDe(TEX.selva, 6, 10) || T.dirt, color: 0x51604a, roughness: 1 }); + const matMata = lam({ map: texDe(TEX.selva, 7, 3) || T.dirt, color: 0x4f7a45, roughness: 1 }); + const matMataEscura = lam({ map: texDe(TEX.selva, 6, 2) || T.dirt, color: 0x39573a, roughness: 1 }); + const matPoste = lam({ map: texDe(TEX.madeira, 1, 3) || T.dirt, color: 0x9c7c55, roughness: 0.95 }); + const matDeck = lam({ map: texDe(TEX.madeira, 3, 3) || T.dirt, color: 0xa98a5f, roughness: 0.92 }); + const matPalha = lam({ map: TEX.palha.map || T.dirt, roughness: 0.98 }); + const matSerragem = lam({ map: TEX.serragem.map || T.dirt, roughness: 1 }); + const matZinco = lam({ map: T.metal, color: 0x7d7a72, metalness: 0.4, roughness: 0.6 }); + const matLona = [0xc23b4e, 0x2f7fbf, 0xd9a521, 0x3f9455].map((c) => lam({ map: texDe(TEX.palha, 2, 2) || T.dirt, color: c, roughness: 0.85 })); + const matCasca = lam({ map: texDe(TEX.madeira, 1, 2) || T.dirt, color: 0x8a7258, roughness: 1 }); + const matTroncoBoiando = lam({ map: texDe(TEX.madeira, 2, 1) || T.dirt, color: 0x7c6a50, roughness: 1 }); + + function addBox(w, h, d, mat, x, y, z, opts = {}) { + const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat); + m.position.set(x, y + h / 2, z); m.castShadow = opts.cast !== false; m.receiveShadow = true; + if (opts.ry) m.rotation.y = opts.ry; + if (opts.rz) m.rotation.z = opts.rz; + if (opts.rx) m.rotation.x = opts.rx; + root.add(m); + if (opts.collide !== false) { + const cs = Math.abs(Math.cos(opts.ry || 0)), sn = Math.abs(Math.sin(opts.ry || 0)); + const ax = w / 2 * cs + d / 2 * sn, az = w / 2 * sn + d / 2 * cs; + colliders.push({ minX: x - ax, maxX: x + ax, minY: y, maxY: y + h, minZ: z - az, maxZ: z + az }); + occluders.push(m); + } + return m; + } + function addCyl(r, h, mat, x, y, z, opts = {}) { + /* deitado (rx ≈ 90°): a barriga fica em y e o topo em y+2r — CylinderGeometry + centraliza no eixo; sem isto o tronco deitado boiava a h/2 do chão. */ + const along = Math.abs(opts.rx || 0) > Math.PI / 4; + const m = new THREE.Mesh(new THREE.CylinderGeometry(r, r, h, opts.seg || 8), mat); + m.rotation.order = 'YXZ'; + m.rotation.y = opts.ry || 0; + m.rotation.x = opts.rx || 0; + m.position.set(x, y + (along ? r : h / 2), z); + m.castShadow = opts.cast !== false; m.receiveShadow = true; + root.add(m); + if (opts.collide !== false) { + const lenX = Math.abs(Math.sin(opts.ry || 0)) * h / 2, lenZ = Math.abs(Math.cos(opts.ry || 0)) * h / 2; + const ext = along ? { x: lenX + r, z: lenZ + r } : { x: r, z: r }; + colliders.push({ minX: x - ext.x, maxX: x + ext.x, minY: y, maxY: y + (along ? 2 * r : h), minZ: z - ext.z, maxZ: z + ext.z }); + occluders.push(m); + } + return m; + } + function addFloor(w, d, mat, x, z, y, ry = 0) { + const m = new THREE.Mesh(new THREE.PlaneGeometry(w, d), mat); + m.rotation.x = -Math.PI / 2; m.rotation.z = ry; + m.position.set(x, y, z); m.receiveShadow = true; + root.add(m); + return m; + } + + /* ── CHÃO: duas faixas de terra — plano a y=0 sob o rio era 0,6 m de MAP1 em toda a lâmina. */ + const FAIXA = HALF_X - RIO_MEIA_LARGURA; + addFloor(FAIXA, HALF_Z * 2, matSelva, -(RIO_MEIA_LARGURA + FAIXA / 2), 0, 0); + addFloor(FAIXA, HALF_Z * 2, matSelva, RIO_MEIA_LARGURA + FAIXA / 2, 0, 0); + const fundo = addFloor(RIO_MEIA_LARGURA * 2, HALF_Z * 2, matFundo, 0, 0, RIO_FUNDO); + fundo.userData.nonSolidSurface = true; + + /* ── ÁGUA VIVA: o idioma do córrego (createWater + sobreposições de reflexo). + Escala de igarapé: célula de 1 m, lâmina preta-esverdeada, fluxo para −z. */ + const agua = createWater(scene, T, 'amazonia', { + nivel: RIO_AGUA, centro: [0, 0], tamanho: [RIO_MEIA_LARGURA * 2 - 0.6, HALF_Z * 2], segmentos: 12, + raso: 0x51755a, fundo: 0x1c2b22, marLonge: 0x40554a, + profEscala: 0.4, espumaFaixa: 0.35, espumaMiolo: 0.12, + profFallback: 0.35, fluxo: [0, 0.09], ampEscala: 0.1, + /* albedo poluído como tMapa (mesmo idioma do córrego): o shader amostra de + verdade E a lâmina conta como área texturizada no texel-check. */ + mapa: TEX.agua.map || T.dirt, mapaEscala: [3, 13.3], mapaForca: 0.55, + parent: root, + }); + agua.mesh.userData.nonSolidSurface = true; + const mapaAgua = lam({ map: texDe(TEX.selva, 2, 12) || T.dirt, color: 0x9fc4ae, transparent: true, opacity: 0.22, roughness: 0.06, metalness: 0.3, depthWrite: false }); + for (const [x, z, w, d, ry] of [[-4.5, -18, 8, 14, 0.04], [4, 10, 9, 16, -0.05], [-2, 32, 7, 12, 0.03]]) { + const r = addFloor(w, d, mapaAgua, x, z, RIO_AGUA + 0.015, ry); + r.userData.nonSolidSurface = true; r.renderOrder = 2; + } + + /* ── LOOK: o céu equatorial úmido mora no LOOK[amazonia] (look.js) — applyLook + devolve hemi/sun e a névoa densa verde-água. */ + const { hemi, sun } = applyLook(scene, T, 'amazonia', { nofog: QP.get('nofog') === '1' }); + + /* ── PONTES: três travessias (z 0 larga, ±24 estreitas); o piso NÃO é colisor — + quem anda é o groundHeightAt (lição do canal do córrego: colisor no vão trava). */ + const ponte = (z, w) => { + addBox(RIO_MEIA_LARGURA * 2 + 3.5, 0.16, w, matDeck, 0, PONTE_Y - 0.16, z, { collide: false }); + for (const sx of [-1, 1]) { + addBox(0.16, 0.5, w, matPoste, sx * (RIO_MEIA_LARGURA + 1.2), PONTE_Y, z, { collide: false, cast: false }); + } + for (const px of [-7, -3.4, 3.4, 7]) addCyl(0.14, PONTE_Y - RIO_FUNDO, matPoste, px, RIO_FUNDO, z, { collide: false }); + }; + ponte(0, PONTE_W); ponte(-24, 2.6); ponte(24, 2.6); + + /* ── PALAFITAS: esteios + dequee a 2,5 m + casa + telhado de palha em duas águas. + O dequee é cover aéreo — o piso de jogo continua no chão. */ + const palafita = ({ x, z, ry, cor }) => { + const w = 4.6, d = 4.2; + const cs = Math.cos(ry), sn = Math.sin(ry); + const local = (lx, lz) => [x + lx * cs - lz * sn, z + lx * sn + lz * cs]; + for (const [lx, lz] of [[-w / 2 + 0.3, -d / 2 + 0.3], [w / 2 - 0.3, -d / 2 + 0.3], [-w / 2 + 0.3, d / 2 - 0.3], [w / 2 - 0.3, d / 2 - 0.3], [0, 0]]) + addCyl(0.16, ESTACA_H, matPoste, ...local(lx, lz), { seg: 6 }); + addBox(w, DECK_E, d, matDeck, x, ESTACA_H, z, { ry, collide: false }); + const parede = lam({ map: texDe(TEX.madeira, 3, 2) || T.dirt, color: cor, roughness: 0.95 }); + addBox(w, 2.7, d, parede, x, ESTACA_H + DECK_E, z, { ry }); + // beiral de palha: dois planos inclinados — nunca colisor, sempre ocorrível + for (const s of [-1, 1]) { + const telhado = new THREE.Mesh(new THREE.BoxGeometry(w + 1.3, 0.09, d / 2 + 0.9), matPalha); + const [cx, cz] = local(0, s * d / 4); + telhado.position.set(cx, ESTACA_H + DECK_E + 2.7 + 0.62, cz); + telhado.rotation.y = ry; telhado.rotation.x = s * 0.42; + telhado.castShadow = true; telhado.receiveShadow = true; + root.add(telhado); occluders.push(telhado); + colliders.push({ minX: cx - (w / 2 + 0.6), maxX: cx + (w / 2 + 0.6), minY: telhado.position.y - 0.3, maxY: telhado.position.y + 0.35, minZ: cz - (d / 2 + 0.6), maxZ: cz + (d / 2 + 0.6) }); + } + // caixa d'água e varal de rede: quintal de ribeirinho + addCyl(0.55, 0.9, matZinco, ...local(w / 2 + 0.9, d / 2 + 0.6), { seg: 10 }); + }; + for (const p of PALAFITAS) palafita(p); + + /* ── MARKET FLUTUANTE: pontões amarrados na margem leste (degrau de 0,28 m — chega-se + andando), barracas de lona e canoas. A bandeira MID mora no pontão norte. */ + const pontoes = [ + { x0: 4.2, x1: 10.4, z0: -2, z1: 3 }, // principal — MID + { x0: 4.0, x1: 9.6, z0: 8, z1: 13 }, + { x0: 4.4, x1: 9.8, z0: -14, z1: -9 }, + ]; + const barraca = (x, z, ry, cor) => { + addBox(1.9, 0.85, 1.1, matDeck, x, 0.4, z, { ry }); + for (const s of [-1, 1]) addCyl(0.07, 2.1, matPoste, x + s * 0.85 * Math.cos(ry), 0.4, z - s * 0.85 * Math.sin(ry), { seg: 5 }); + const tarp = new THREE.Mesh(new THREE.BoxGeometry(2.6, 0.05, 1.9), cor); + tarp.position.set(x, 2.55, z); tarp.rotation.y = ry; tarp.rotation.z = 0.16; + tarp.castShadow = true; root.add(tarp); occluders.push(tarp); + }; + const PONTAO_Y = 0.28; + const pontao = ({ x0, x1, z0, z1 }, stalls) => { + const cx = (x0 + x1) / 2, cz = (z0 + z1) / 2, w = x1 - x0, d = z1 - z0; + addBox(w, 0.14, d, matDeck, cx, PONTAO_Y - 0.14, cz, { collide: false }); + /* ancoragem: pinta acima da água mas não é degrau nem cover — o topo dela + fora do pé do pontão seria 1 m de "corpo dentro de sólido" pro MAP1. */ + for (const sx of [x0 + 0.3, x1 - 0.3]) for (const sz of [z0 + 0.3, z1 - 0.3]) { + const a = addCyl(0.2, 0.9, matPoste, sx, -0.5, sz, { seg: 6, collide: false }); + a.userData.nonSolidSurface = true; + } + stalls(); + }; + pontao(pontoes[0], () => { + barraca(5.6, 0.4, 0.24, matLona[0]); barraca(7.4, 1.8, -0.18, matLona[1]); barraca(9.2, 0.2, 0.31, matLona[2]); + }); + pontao(pontoes[1], () => { barraca(5.4, 9.2, -0.26, matLona[3]); barraca(7.8, 11.6, 0.2, matLona[0]); }); + pontao(pontoes[2], () => { barraca(5.8, -12.6, 0.15, matLona[2]); barraca(8.2, -10.8, -0.29, matLona[1]); }); + // canoas amarradas: casco de tábua com banco + const canoa = (x, z, ry, cor) => { + addBox(3.4, 0.5, 0.9, lam({ map: texDe(TEX.madeira, 3, 1) || T.dirt, color: cor, roughness: 0.9 }), x, RIO_AGUA - 0.2, z, { ry }); + addBox(0.9, 0.08, 0.8, matPoste, x, RIO_AGUA + 0.3, z, { ry, collide: false, cast: false }); + }; + canoa(-3.2, -24.5, 0.34, 0xb54a3c); canoa(2.6, 18.5, -0.22, 0x3c6fb0); canoa(-5.2, 6.5, 1.35, 0xb5892e); + canoa(11.8, -4.6, 0.42, 0x4a8a5c); canoa(-2.2, -6.5, 1.52, 0x8a5a2e); + // booms de lenha: troncos amarrados boiando — cover no meio do canal + for (const b of BOOMS) for (let i = 0; i < b.n; i++) { + addCyl(0.35, 3.6, matTroncoBoiando, b.x0 + i * 1.18, RIO_AGUA - 0.32, b.z + (i % 2) * 0.5, { rx: Math.PI / 2, seg: 7 }); + } + + /* ── MADEIREIRA ABANDONADA (o galpão do CTF): telhadão de zinco sobre postes, + sem paredes — serragem no chão, serras, pilhas de tora e pranchas empilhadas. */ + const MX = -24.5; + addFloor(15, 8, matSerragem, MX, 0, 0.012); + for (const px of [-7, -3.5, 0, 3.5, 7]) for (const pz of [-3.6, 3.6]) + addCyl(0.22, 5.2, matPoste, MX + px, 0, pz, { seg: 7 }); + for (const s of [-1, 1]) { + const telhado = new THREE.Mesh(new THREE.BoxGeometry(16.4, 0.12, 5.4), matZinco); + telhado.position.set(MX, 5.7, s * 2.55); telhado.rotation.x = s * 0.3; + telhado.castShadow = true; telhado.receiveShadow = true; + root.add(telhado); occluders.push(telhado); + colliders.push({ minX: MX - 8.2, maxX: MX + 8.2, minY: 5.4, maxY: 6.1, minZ: s * 2.55 - 2.9, maxZ: s * 2.55 + 2.9 }); + } + // serras/bancadas: mesas pesadas de madeira + addBox(4.2, 0.95, 1.3, matDeck, MX - 1.5, 0, -1.6, { ry: 0.06 }); + addBox(3.2, 0.95, 1.2, matDeck, MX + 3.4, 0, 1.8, { ry: -0.12 }); + addBox(2.2, 0.85, 1.1, matDeck, MX - 5, 0, 2.2, { ry: 0.3 }); + // pilhas de tora: pirâmides de 3+2 + const pilhaTora = (x, z, ry) => { + const cs = Math.cos(ry), sn = Math.sin(ry); + for (let i = 0; i < 3; i++) addCyl(0.38, 4, matCasca, x + (i - 1) * 0.8 * cs, 0, z + (i - 1) * 0.8 * sn, { rx: Math.PI / 2, ry, seg: 7 }); + for (let i = 0; i < 2; i++) addCyl(0.38, 4, matCasca, x + (i - 0.5) * 0.8 * cs, 0.66, z + (i - 0.5) * 0.8 * sn, { rx: Math.PI / 2, ry, seg: 7 }); + }; + pilhaTora(MX + 7.5, -6, 0.12); pilhaTora(MX + 7.5, 6, -0.18); pilhaTora(MX, 18, 0.24); pilhaTora(24, -33, -0.3); + // pranchas empilhadas + const pranchas = (x, z, ry) => { for (let i = 0; i < 4; i++) addBox(2.6, 0.14, 0.9, matDeck, x, 0.14 * i, z, { ry: ry + i * 0.04 }); }; + pranchas(MX - 6.5, -4.5, 0.34); pranchas(MX + 4.5, -4.8, -0.28); pranchas(20.5, 30, 0.18); + // scrap da madeireira: pneus e entulho herdam o vocabulário do ferro-velho + for (const [id, x, z, h, ry] of [['pilha_pneus', MX - 8.5, 5.5, 1.1, 0.2], ['dumpster', MX + 9.8, 0, 1.4, -0.34], ['tires', 12.5, -33.5, 0.8, 0.4], ['pilha_pneus', 25, 20, 1.1, -0.26]]) { + if (hasProp(id)) { const p = placeProp(id, { x, z, y: 0, targetH: h, ry }); if (p) { root.add(p); colliders.push({ minX: x - 1.2, maxX: x + 1.2, minY: 0, maxY: h, minZ: z - 1.2, maxZ: z + 1.2 }); occluders.push(p); } } + } + + /* ── CERCA VIVA / MATA DE FUNDO: parede verde com ruído de yaw e duas alturas — + é o horizonte fechado do mapa e a massa alta do ALT1. */ + const matas = []; + for (let i = 0; i < 12; i++) { + const x = -HALF_X + 1.3 + i * (HALF_X * 2 - 2.6) / 11; + matas.push([x, -HALF_Z + 0.9, 8 + (i % 3) * 1.5, (i * 0.37) % 0.5 - 0.25]); + matas.push([x, HALF_Z - 0.9, 6.5 + (i % 4) * 1.4, -((i * 0.29) % 0.5) + 0.25]); + } + for (let j = 0; j < 10; j++) { + const z = -HALF_Z + 4.5 + j * (HALF_Z * 2 - 9) / 9; + matas.push([-HALF_X + 1.2, z, 7 + (j % 3) * 1.6, ((j * 0.31) % 0.5) - 0.25]); + matas.push([HALF_X - 1.2, z, 9 + (j % 2) * 1.3, -((j * 0.23) % 0.45) + 0.22]); + } + for (const [x, z, h, ry] of matas) { + addBox(6.2, h, 2.4, (h + x + z) % 2 > 1 ? matMata : matMataEscura, x, 0, z, { ry }); + // copa em sanfona quebra a silhueta de muro + addBox(3.4, 2.2, 2.8, matMataEscura, x + Math.sin(ry * 7 + z) * 1.4, h, z + Math.cos(ry * 5 + x) * 1.1, { ry: ry + 0.4, collide: false, cast: true }); + } + + /* ── SUMAÚMAS: tronco alto + copa em guardanapo — seis marcos de silhueta. */ + const sumauma = (x, z, s) => { + addCyl(0.75 * s, 8.4 * s, matCasca, x, 0, z, { seg: 9 }); + for (let i = 0; i < 5; i++) { + const a = i * Math.PI * 2 / 5 + x; + const copa = new THREE.Mesh(new THREE.SphereGeometry(2.6 * s, 8, 6), matMata); + copa.position.set(x + Math.cos(a) * 2.4 * s, (8.4 + Math.sin(i * 2.1) * 1.2) * s, z + Math.sin(a) * 2.4 * s); + copa.scale.y = 0.62; copa.castShadow = true; copa.receiveShadow = true; + root.add(copa); occluders.push(copa); + colliders.push({ minX: copa.position.x - 2, maxX: copa.position.x + 2, minY: copa.position.y - 1.2, maxY: copa.position.y + 1.2, minZ: copa.position.z - 2, maxZ: copa.position.z + 2 }); + } + }; + sumauma(-26, -33, 1.15); sumauma(26, -33, 0.95); sumauma(-26, 33, 1.05); sumauma(26, 33, 1.2); sumauma(-16, 38, 0.9); sumauma(16, -38, 0.85); + + /* ── SUB-BOSQUE: props de mata do acervo quando o GLB carrega, bucha procedural + quando não carrega — o mapa não pode depender de download para ter volume. */ + const FX = [ + ['samambaia', 1.1], ['heliconia', 1.3], ['planta_corrego_taboa', 1.6], ['planta_corrego_taioba', 0.9], + ['grama_corrego_01', 0.8], ['grama_corrego_02', 0.7], + ]; + const moita = (x, z, s) => { + const m = new THREE.Mesh(new THREE.IcosahedronGeometry(0.55 * s, 1), matMata); + m.position.set(x, 0.3 * s, z); m.scale.y = 0.75; m.castShadow = true; m.receiveShadow = true; + m.userData.nonSolidSurface = true; + root.add(m); + }; + let fi = 0; + for (const [x, z] of [[-29, -20], [-28.2, -6], [-29.5, 10], [-28, 24], [29, -18], [28.3, -4], [29.4, 12], [28.1, 26], [-12, -32], [12, 33], [-11, 34], [11, -33], [-22, -27], [22, 28], [-20, 28], [19, -28]]) { + const [id, h] = FX[fi++ % FX.length]; + if (hasProp(id)) { const p = placeProp(id, { x, z, y: 0, targetH: h * (0.85 + (fi % 4) * 0.1), ry: fi * 1.7 }); if (p) { p.traverse((o) => { if (o.isMesh) o.userData.nonSolidSurface = true; }); root.add(p); } } + else moita(x + 1.2, z + 0.8, h); + if (fi % 2 === 0) moita(x - 1.4, z - 0.6, h * 0.8); + } + // troncos caídos no chão de jogo: cover baixo orgânico + const troncoNoChao = (x, z, ry, len = 4.4, r = 0.42) => { + addCyl(r, len, matCasca, x, 0, z, { rx: Math.PI / 2, ry, seg: 8 }); + }; + troncoNoChao(-13, -12, 0.32); troncoNoChao(13, 13, -0.38); troncoNoChao(-12, 30, 1.22); troncoNoChao(12, -30, 0.44, 5); + troncoNoChao(-22, 30, 0.18); troncoNoChao(22, 10, 1.38); troncoNoChao(-20, -30, 0.5); troncoNoChao(20, 32, -0.2); + + /* ── FAUNA ESTÁTICA: jacaré no igarapé, capivaras na margem (o sistema do córrego). */ + const jacare = placeFauna('jacare', { x: 6.8, y: RIO_FUNDO, z: -30.5, ry: 2.8, targetLen: 1.8, submerge: 0.1 }); + if (jacare) root.add(jacare); + const capivara = placeFauna('capivara', { x: -10.8, y: 0.02, z: 14.5, ry: 0.6, targetLen: 1.0 }); + if (capivara) root.add(capivara); + const capivara2 = placeFauna('capivara', { x: 11.2, y: 0.02, z: -20.5, ry: -0.9, targetLen: 0.95 }); + if (capivara2) root.add(capivara2); + + /* ── PICKUPS: arsenal completo por spawn (veto do dono) + miolo; materiais de arma + COMPARTILHADOS — um material novo por pickup derruba o SUP1. */ + const GM = { black: lam({ color: 0x202735 }), steel: lam({ color: 0xaab4c0 }), wood: lam({ color: 0x6b4a2a }), verde: lam({ color: 0x315b43 }) }; + const place = (kind, x, z, yaw = 0) => { + const g = new THREE.Group(); + const long = ['awp', 'ak', 'm4', 'shotgun', 'mp5'].includes(kind); + const b1 = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.1, long ? 1.0 : 0.38), kind === 'awp' ? GM.verde : GM.black); + b1.position.y = 0.1; g.add(b1); + const b2 = new THREE.Mesh(new THREE.BoxGeometry(0.11, 0.18, long ? 0.28 : 0.12), kind === 'shotgun' ? GM.wood : GM.steel); + b2.position.set(0, 0.03, long ? 0.38 : 0.12); g.add(b2); + g.position.set(x, groundHeightAt(x, z) + 0.06, z); g.rotation.y = yaw; + root.add(g); pickups.push({ x, z, kind, weapon: kind, readyAt: 0, mesh: g }); + }; + const arsenal = ['awp', 'ak', 'm4', 'shotgun', 'mp5', 'deagle', 'pistol']; + /* Arsenal no pé de cada spawn — o rio corre no meio do mapa, os times nascem + nas cabeceras NW e SE, em terra (nascer na água foi folga 0,45 m no MAP2B). */ + for (const sz of [-1, 1]) arsenal.forEach((kind, i) => place(kind, sz < 0 ? -20 + i * 2 : 12 + i * 2, sz * 39, sz < 0 ? 0.6 : Math.PI - 0.6)); + // miolo: market, madeireira e pontes estreitas + place('ak', 5.6, 1.4, 1.2); place('awp', 9.2, -0.2, 0.4); + place('shotgun', MX + 1, -1.4, 2.2); place('mp5', MX, 18.5, -0.6); + place('deagle', 0, -24.2, 0); place('m4', 0, 24.2, Math.PI); + + /* ── NAVEGAÇÃO: grade própria (idioma do parque), rio vadeável incluso. Nó sem + vizinho alcançável é PODADO: ilhado reprova o MC3 do map-contrato. */ + const blocked = (x, z, inflate = 0.45) => colliders.some((c) => c.minY < 1.6 && c.maxY > 0.15 + && x > c.minX - inflate && x < c.maxX + inflate && z > c.minZ - inflate && z < c.maxZ + inflate); + const STEP = 3.2; + const crusos = []; + for (let x = -HALF_X + 2.5; x <= HALF_X - 2.5; x += STEP) for (let z = -HALF_Z + 2.5; z <= HALF_Z - 2.5; z += STEP) if (!blocked(x, z)) crusos.push({ x, z }); + const segClear = (a, b) => { for (let i = 1; i < 6; i++) { const t = i / 6; if (blocked(a.x + (b.x - a.x) * t, a.z + (b.z - a.z) * t, 0.2)) return false; } return true; }; + const vizinhos = (i) => { + const out = []; + for (let j = 0; j < crusos.length; j++) { + if (i === j) continue; + const dx = crusos[i].x - crusos[j].x, dz = crusos[i].z - crusos[j].z; + if (dx * dx + dz * dz < STEP * STEP * 2.45 && segClear(crusos[i], crusos[j])) out.push(j); + } + return out; + }; + const adjCrusos = crusos.map((_, i) => vizinhos(i)); + const componente = new Uint8Array(crusos.length); + { + const fila = [0]; componente[0] = 1; + while (fila.length) { + const i = fila.pop(); + for (const j of adjCrusos[i]) if (!componente[j]) { componente[j] = 1; fila.push(j); } + } + } + const mantidos = []; + for (let i = 0; i < crusos.length; i++) if (componente[i]) mantidos.push(i); + const remapa = new Int16Array(crusos.length).fill(-1); + mantidos.forEach((i, novo) => { remapa[i] = novo; }); + const nodes = mantidos.map((i) => crusos[i]); + const adj = mantidos.map((i) => adjCrusos[i].map((j) => remapa[j])); + function nearestWaypoint(x, z) { let best = 0, bd = Infinity; for (let i = 0; i < nodes.length; i++) { const dx = nodes[i].x - x, dz = nodes[i].z - z, d = dx * dx + dz * dz; if (d < bd) { bd = d; best = i; } } return best; } + function findPath(fromIdx, toIdx) { + if (fromIdx === toIdx) return [toIdx]; + const prev = new Int16Array(nodes.length).fill(-1), queue = [fromIdx]; prev[fromIdx] = fromIdx; + while (queue.length) { + const n = queue.shift(); + for (const next of adj[n]) if (prev[next] === -1) { prev[next] = n; if (next === toIdx) { const path = [next]; let cur = n; while (cur !== fromIdx) { path.unshift(cur); cur = prev[cur]; } path.unshift(fromIdx); return path; } queue.push(next); } + } + return [fromIdx]; + } + + /* ── VIDA: capivaras passeando na margem, galinha no quintal da palafita, + papagaio de poleiro na barraca do market, rato e pomba na madeireira. */ + const ambience = createFavelaAmbience(root, { + map: 'amazonia', low: LOWQ, + /* posições fora dos AABBs das bancadas/pilhas da madeireira (AR3): o galpão é + serragem livre entre os móveis, os bichos ficam nas clareiras dele. */ + rats: [ + { pos: [-27.5, 0, -4.8], to: [-26.5, 0, -5.6], phase: 0.3 }, + { pos: [-19.5, 0, -3.4], to: [-18.5, 0, -2.6], phase: 1.8 }, + ], + pigeons: [ + { mode: 'ground', pos: [-22.5, 0, -6.2], phase: 0.9 }, + { mode: 'ground', pos: [-29.5, 0, -6.5], phase: 2.1 }, + ], + chickens: [{ pos: [16.8, 0, 7.4], to: [18.4, 0, 8.8], phase: 1.4 }], + parrots: [ + { pos: [5.6, 2.62, 0.4], phase: 0.6 }, // poleiro da barraca do market + { pos: [21.6, 2.62, 17.4], phase: 2.4 }, + { pos: [-26, 0.95, 20.6], phase: 3.1 }, // caixa d'água da palafita oeste + ], + dogs: [{ pos: [-14.8, 0, -20.4], to: [-13.2, 0, -19.2], phase: 0.7 }], + /* elenco da mata (PR #439): 9 espécies Mint com vida no ambientlife.js; posições + fora de AABB de colisor (AR3) — caixa d'água, poste, beiral e tronco por altura. */ + botos: [{ pos: [2.5, -0.38, -12], to: [2.5, -0.38, 14], phase: 0.4, mode: 'swim' }], + piranhas: Array.from({ length: 7 }, (_, i) => ( + { pos: [-4, -0.15, -20.5], radius: [1.2 + (i % 3) * 0.35, 1], phase: i * 0.87 } + )), + araras: [ + { pos: [4.77, 2.5, 0.6], phase: Math.PI }, // poste da barraca do MID + { pos: [7.39, 2.5, -11.04], phase: Math.PI - 0.5 }, // poste do pontão sul + ], + tucanos: [ + { pos: [-11.41, 0.92, 22.16], phase: 0 }, // caixa d'água da palafita oeste + { pos: [23.76, 0.92, 20.15], phase: Math.PI }, // caixa d'água da palafita leste + ], + preguicas: [{ pos: [16.2, 5.6, -11.43], phase: 0 }], // beiral da palafita (15.5, -9) + macacos: [ + { pos: [MX + 0.5, 6.05, 3.5], to: [MX + 7, 6.05, 3.5], phase: 0.8 }, // cumeeira do galpão + { pos: [MX - 1.5, 0.95, -1.6], to: [MX + 3.4, 0.95, 1.8], phase: 2.2 }, // bancadas do serrado + ], + oncas: [{ pos: [12, 0.86, -30.2], phase: 2.4 }], // tronco caído da margem leste + antas: [ + { pos: [-26.5, 0, -28], to: [-25, 0, -16], phase: 0.5 }, + { pos: [26, 0, 12], to: [27, 0, 16], phase: 2.8 }, + ], + carcaras: [{ pos: [-20.5, 0, -6.8], to: [-19.8, 0, -6.2], phase: 1.9 }], + }); + + function groundHeightAt(x, z) { + for (const p of pontoes) if (x >= p.x0 && x <= p.x1 && z >= p.z0 && z <= p.z1) return PONTAO_Y; + for (const pz of [0, -24, 24]) if (Math.abs(z - pz) < (pz === 0 ? PONTE_W : 2.6) / 2 && Math.abs(x) <= RIO_MEIA_LARGURA + 1.75) return PONTE_Y; + const ax = Math.abs(x); + if (ax > RIO_MEIA_LARGURA) return 0; + if (ax <= RIO_CAMPO) return RIO_FUNDO; + return RIO_FUNDO + (ax - RIO_CAMPO) / (RIO_MEIA_LARGURA - RIO_CAMPO) * (RIO_MARGEM - RIO_FUNDO); + } + const slowAt = (x, z) => Math.abs(x) <= RIO_MEIA_LARGURA + 0.6 && Math.abs(z) < HALF_Z - 2 + && ![0, -24, 24].some((pz) => Math.abs(z - pz) < (pz === 0 ? PONTE_W : 2.6) / 2 + 0.4); + + return { + root, colliders, occluders, decalSolids: [root], groundHeightAt, slowAt, spawns: { + /* 0,4 m além da face da mata girada: o AABB conservador da copa com yaw + projeta a quina ~0,5 m para dentro da caixa e a folga do MAP2B mede + contra o AABB, não contra a malha. */ + E: [[-15, -40], [-12, -39.3], [-18, -39.3], [-13.5, -37.9], [-16.5, -37.9]].map(([x, z]) => ({ x, z, yaw: 0.6 })), + B: [[15, 40], [12, 39.3], [18, 39.3], [13.5, 37.9], [16.5, 37.9]].map(([x, z]) => ({ x, z, yaw: Math.PI - 0.6 })), + }, + ctfPoints: [ + { id: 'E', label: 'PALAFITAS', x: 20.5, z: -28 }, + { id: 'MID', label: 'MERCADO FLUTUANTE', x: 7.4, z: 0.6 }, + { id: 'B', label: 'MADEIREIRA', x: MX, z: 18 }, + ], + pickups, sun, hemi, ambience, + sound: { + loops: [ + { src: AMB_LOOPS.corrego, pos: [0, 0.3, -22], radius: 26, vol: 0.38 }, + { src: AMB_LOOPS.corrego, pos: [0, 0.3, 22], radius: 26, vol: 0.38 }, + { src: AMB_LOOPS.passaros, pos: [0, 5, 0], radius: 85, vol: 0.3 }, + { src: AMB_LOOPS.grilos, pos: [0, 3, 0], radius: 85, vol: 0.14 }, + ], + bioma: 'campo', + }, + update(dt) { agua.update(dt); }, + waypoints: { nodes, adj }, nearestWaypoint, findPath, + bounds: { minX: -HALF_X + 0.8, maxX: HALF_X - 0.8, minZ: -HALF_Z + 0.8, maxZ: HALF_Z - 0.8 }, + }; +} diff --git a/public/js/maps.js b/public/js/maps.js index cb3b74c6..1005ecaf 100644 --- a/public/js/maps.js +++ b/public/js/maps.js @@ -16,6 +16,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 { buildAmazonia, AMAZONIA_PROPS, AMAZONIA_AMBIENCE } from './map_amazonia.js'; /* IDS SEM NOME DE COUNTER-STRIKE (rodada de 11/08). ═══════════════════════════════════════════════════════════════════════════════════ @@ -92,6 +93,10 @@ 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 }, + /* amazonia: retrato do "Treta no Vietnã" (PR #375, rejeitado) como comunidade + ribeirinha — igarapé, palafitas de palha, market flutuante e madeireira. Os + alias `fy_amazonia` e `vietnam` mantêm link/banco do mapa antigo chegando aqui. */ + amazonia: { name: 'Treta na Amazônia', build: buildAmazonia, props: AMAZONIA_PROPS, ambience: AMAZONIA_AMBIENCE, ctfMode: true }, }; export const MAP_IDS = Object.keys(MAPS); export const DEFAULT_MAP = 'praca_poderes'; @@ -128,6 +133,11 @@ export const ALIAS_MAPA = { lajes: 'lajes', corrego: 'corrego', mansao: 'mansao', + /* map2/amazonia: o "Treta no Vietnã" do PR #375 nunca entrou no registro desta + árvore, mas links de partida e linhas de banco gravadas no período de review + citam `vietnam`; `fy_amazonia` cobre o alias do id próprio na época do PR. */ + fy_amazonia: 'amazonia', + vietnam: 'amazonia', }; export function resolveMapId(id) { diff --git a/public/models/ambient/anta.glb b/public/models/ambient/anta.glb new file mode 100644 index 00000000..5bac70ad Binary files /dev/null and b/public/models/ambient/anta.glb differ diff --git a/public/models/ambient/arara_vermelha.glb b/public/models/ambient/arara_vermelha.glb new file mode 100644 index 00000000..b44ac114 Binary files /dev/null and b/public/models/ambient/arara_vermelha.glb differ diff --git a/public/models/ambient/boto_amazonia.glb b/public/models/ambient/boto_amazonia.glb new file mode 100644 index 00000000..db4b3f05 Binary files /dev/null and b/public/models/ambient/boto_amazonia.glb differ diff --git a/public/models/ambient/carcara.glb b/public/models/ambient/carcara.glb new file mode 100644 index 00000000..f8bc5a4c Binary files /dev/null and b/public/models/ambient/carcara.glb differ diff --git a/public/models/ambient/macaco_prego.glb b/public/models/ambient/macaco_prego.glb new file mode 100644 index 00000000..d1fa514f Binary files /dev/null and b/public/models/ambient/macaco_prego.glb differ diff --git a/public/models/ambient/onca_pintada.glb b/public/models/ambient/onca_pintada.glb new file mode 100644 index 00000000..0fe071ad Binary files /dev/null and b/public/models/ambient/onca_pintada.glb differ diff --git a/public/models/ambient/piranha.glb b/public/models/ambient/piranha.glb new file mode 100644 index 00000000..478af054 Binary files /dev/null and b/public/models/ambient/piranha.glb differ diff --git a/public/models/ambient/preguica.glb b/public/models/ambient/preguica.glb new file mode 100644 index 00000000..a5f967bd Binary files /dev/null and b/public/models/ambient/preguica.glb differ diff --git a/public/models/ambient/tucano.glb b/public/models/ambient/tucano.glb new file mode 100644 index 00000000..59f5acf4 Binary files /dev/null and b/public/models/ambient/tucano.glb differ diff --git a/tools/eval/amazonia-check.mjs b/tools/eval/amazonia-check.mjs new file mode 100644 index 00000000..8daa025f --- /dev/null +++ b/tools/eval/amazonia-check.mjs @@ -0,0 +1,142 @@ +/* ============================================================================ + amazonia-check.mjs — A RÉGUA DO "NÃO PARECE LOW POLY" DA FRENTE AMAZONIA. + ---------------------------------------------------------------------------- + POR QUE EXISTE + + O PR #375 ("Treta no Vietnã") foi rejeitado pelo dono com a alegação "não + parece low poly" — frase que, sem instrumento, vira gosto. Esta régua traduz a + alegação em QUATRO medidas no mundo construído (bootGame real, node puro), + escolhidas para NÃO duplicar cláusula existente (LIÇÃO 2 do docs/LICOES.md: + dois limiares para o mesmo conceito é o instrumento discordando de si): + + AMZ1 variedade de MATERIAL: nº de materiais distintos COM textura (`map`) + na cena. O mapa-novo-gate já cobra FRAÇÃO de materiais texturizados + (SUP1); aqui o que se mede é DIVERSIDADE — o sintoma do "low poly" + que dói no olho não é superfície lisa, é o MESMO material em tudo. + AMZ2 densidade de cover por área: colisores de pé-de-chão (minY < 1,6 m, + altura útil ≥ 0,6 m) por 100 m² dos bounds. O MAP5 do invariants.mjs + mede desequilíbrio RELATIVO à mediana dos quadrantes (e só cobra no + loja_h); aqui é o piso ABSOLUTO — mapa inteiro vazio também passa + na relativa. + AMZ3 água VIVA: o igarapé tem que ter a água do idioma do córrego + (mesh com `aguaViva` registrado em scene.userData.waters), não um + plano azul parado. + AMZ4 fauna ANIMADA em população: o piso sobe com o elenco (PR #439): + ≥ 12 espécies e ≥ 24 bichos. Medido no elenco entregue: 28 bichos + de 14 espécies (saída deste script; a AR2 do ambience-registry + pede 2 espécies — o piso da frente amazonia é maior porque o dono + pediu "mata viva"). + + PROCEDÊNCIA DOS PISOS (Lei 2 — teto sem procedência é opinião) + AMZ1 ≥ 12 e AMZ2 ≥ 2,0/100 m²: medidos nesta frente ANTES do polimento + (primeira build que passou no MAP1): 31 materiais texturizados distintos e + 152 colisores de pé-de-chão em 5.391 m² de bounds = 2,82/100 m² (saída + deste script, reproduzível no commit "wip: amazonia sobe"). Os pisos ficam + a ~39% e ~71% do medido: piso colado no valor reprova por ruído (a lição + do ORT1 do mapa-novo-gate) e o mutante é quem prova que o piso morde. + AMZ3 e AMZ4: binário/populacional, lidos do contrato dos módulos + (water.js marca `aguaViva` em scene.userData.waters; ambientlife.js + expõe `ambience.animals`). AMZ4 piso 24/12: medido 28 bichos de 14 + espécies no elenco da frente (PR #439, 9 espécies Mint + as 5 clássicas + do mapa); a folga 4/2 absorve um indivíduo a menos por espécie sem + deixar o mapa virar deserto de bicho. + + AS MUTAÇÕES QUE A DEIXAM VERMELHA (Lei 3 — se não morde, não existe) + --mutante=monocromia ... troca TODO material por um só chapado -> AMZ1 + --mutante=deserto ..... mantém 1 a cada 4 colisores de cover -> AMZ2 + --mutante=agua-morta ... desregistra as águas vivas da cena -> AMZ3 + --mutante=fauna-unica .. deixa só os ratos -> AMZ4 + Cada mutante tem que acender SÓ a cláusula dele (mutação que acende + duas não prova nenhuma). + + USO + node tools/eval/amazonia-check.mjs + node tools/eval/amazonia-check.mjs --mutante=monocromia + ============================================================================ */ +import { THREE, MAPS, initTextures, bootGame } from './harness.mjs'; + +const MUT = (process.argv.find((a) => a.startsWith('--mutante=')) || '').split('=')[1] || ''; +const MUTANTES = ['monocromia', 'deserto', 'agua-morta', 'fauna-unica']; +if (MUT && !MUTANTES.includes(MUT)) { + console.error(`mutante desconhecido: ${MUT}\nconhecidos: ${MUTANTES.join(' | ')}`); + process.exit(2); +} + +const PISO_MATS = 12; // AMZ1 — ver procedência no cabeçalho +const PISO_COVER = 2.0; // AMZ2 — colisores de cover por 100 m² de bounds +const PISO_FAUNA_N = 24, PISO_FAUNA_ESP = 12; // AMZ4 — real 28/14 (elenco PR #439), folga 4/2 + +const SEED = 13007; // mesma do mapa-novo-gate: mesmos props sortidos + +const g = bootGame('amazonia', { textures: initTextures(), ctf: true, seed: SEED }); +const W = g.world; +W.root.updateMatrixWorld(true); +const scene = g.scene; + +/* ── AMZ1: materiais distintos com textura ── */ +const mats = new Map(); +W.root.traverse((o) => { + if (!o.isMesh || o.visible === false) return; + const arr = Array.isArray(o.material) ? o.material : [o.material]; + for (const m of arr) { + if (!m || m.visible === false) continue; + const e = mats.get(m.uuid) || { map: !!m.map, cor: m.color?.getHex() }; + mats.set(m.uuid, e); + } +}); +let matsTexturizados = [...mats.values()].filter((e) => e.map).length; +if (MUT === 'monocromia') { + const unico = new THREE.MeshStandardMaterial({ color: 0x6b7a5e }); + W.root.traverse((o) => { if (o.isMesh) o.material = unico; }); + matsTexturizados = 1; +} + +/* ── AMZ2: densidade de cover por área de bounds ── */ +const B = W.bounds; +const areaBounds = (B.maxX - B.minX) * (B.maxZ - B.minZ); +const coverDe = (lista) => lista.filter((c) => c.minY < 1.6 && c.maxY - c.minY >= 0.6 + && (c.maxX - c.minX) * (c.maxZ - c.minZ) <= 60); +let cover = coverDe(W.colliders); +if (MUT === 'deserto') cover = cover.filter((_, i) => i % 4 === 0); +const densCover = cover.length / areaBounds * 100; + +/* ── AMZ3: água viva do igarapé (createWater registra em scene.userData.waters; + a lâmina mora em world.root — não somar as duas fontes, é o mesmo mesh) ── */ +let aguasVivas = (scene.userData.waters || []).length; +if (MUT === 'agua-morta') aguasVivas = 0; + +/* ── AMZ4: fauna animada ── */ +let animais = W.ambience?.animals || []; +if (MUT === 'fauna-unica') animais = animais.filter((a) => a.type === 'rat'); +const especies = new Set(animais.map((a) => a.type)).size; + +/* ── veredito ── */ +const clausulas = [ + { id: 'AMZ1 variedade de material texturizado', ok: matsTexturizados >= PISO_MATS, + valor: `${matsTexturizados} materiais (piso ${PISO_MATS})` }, + { id: 'AMZ2 densidade de cover', ok: densCover >= PISO_COVER, + valor: `${densCover.toFixed(2)}/100 m² de bounds · ${cover.length} colisores de pé-de-chão em ${areaBounds.toFixed(0)} m² (piso ${PISO_COVER})` }, + { id: 'AMZ3 água viva do igarapé', ok: aguasVivas >= 1, + valor: `${aguasVivas} lâmina(s) com aguaViva (createWater da water.js)` }, + { id: 'AMZ4 fauna animada', ok: animais.length >= PISO_FAUNA_N && especies >= PISO_FAUNA_ESP, + valor: `${animais.length} bichos de ${especies} espécies (pisos ${PISO_FAUNA_N}/${PISO_FAUNA_ESP})` }, +]; + +console.log(`AMAZONIA — régua da frente ${MUT ? `[mutante: ${MUT}]` : ''}`); +for (const c of clausulas) console.log(` ${c.ok ? 'PASSA' : 'FALHA'} ${c.id.padEnd(38)} ${c.valor}`); +const vermelhas = clausulas.filter((c) => !c.ok); + +if (MUT) { + const esperado = { monocromia: 'AMZ1', deserto: 'AMZ2', 'agua-morta': 'AMZ3', 'fauna-unica': 'AMZ4' }[MUT]; + if (vermelhas.length !== 1 || !vermelhas[0].id.startsWith(esperado)) { + console.error(`\nMUTANTE ${MUT} ${vermelhas.length ? `acendeu ${vermelhas.map((v) => v.id.split(' ')[0]).join(', ')} em vez de ${esperado}` : 'SOBREVIVEU'} — a régua não mede o que diz medir.`); + process.exit(1); + } + console.log(`\nMUTANTE MORDIDO: ${MUT} -> ${esperado}`); + process.exit(0); +} +if (vermelhas.length) { + console.error(`\n✗ AMAZONIA: ${vermelhas.length} cláusula(s) vermelha(s) — "low poly" tem número, e ele está aqui.`); + process.exit(1); +} +console.log('\n✓ AMAZONIA ok — variedade, cover, água viva e fauna nos pisos da frente.'); diff --git a/tools/eval/ambience-registry-check.mjs b/tools/eval/ambience-registry-check.mjs index fef261fa..03e05684 100644 --- a/tools/eval/ambience-registry-check.mjs +++ b/tools/eval/ambience-registry-check.mjs @@ -55,6 +55,9 @@ const BIOMA_FAUNA = { (córrego/atacadão), papagaio de poleiro (mansão/parque) */ praca_poderes: ['armadillo'], atacadao_treta: ['cockroach'], mansao: ['parrot'], parque_treta: ['parrot'], + /* map2/amazonia: mata tem papagaio de poleiro no market e galinha de quintal + na palafita — espécie-chave herda o bioma, não o gosto do autor do mapa. */ + amazonia: ['parrot', 'chicken'], }; const FAUNA2 = new Set(['armadillo', 'cockroach', 'parrot']); diff --git a/tools/eval/map_check.json b/tools/eval/map_check.json index f5986f4b..0db3b4fb 100644 --- a/tools/eval/map_check.json +++ b/tools/eval/map_check.json @@ -1,5 +1,5 @@ { - "gerado": "2026-08-25T01:19:40.585Z", + "gerado": "2026-08-25T17:47:13.676Z", "criterios": { "map1": "corpo dentro de sólido: raycast do peito (1.4 m) até o chão; sólido com topo > 0.3 m acima do chão local reprova", "map2": "exposição: fração dos pontos andáveis a ≥ 25 m com _losClear até a cabeça (1.62 m) de quem nasce",