diff --git a/ARCH.generated.md b/ARCH.generated.md index 78d40edb..53ab9d98 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** | 59 arquivos `.js`, 41.468 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. @@ -25,10 +25,10 @@ Comandos e scripts atuais do quality gate: ```bash -npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval: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 +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:penitenciariavida 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..99ca3d38 100644 --- a/README.md +++ b/README.md @@ -34,17 +34,17 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 41.159 linhas em 59 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| Código do jogo | 41.468 linhas em 59 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` | | GLBs de personagem | 63 | `git ls-files 'public/models/characters/*.glb' \| wc -l` | -| Props em GLB | 151 | `git ls-files 'public/models/props/*.glb' \| wc -l` | +| Props em GLB | 153 | `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` | | 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) | @@ -322,7 +322,7 @@ Os mapas registrados, e em que modo cada um abre: | `atacadao_treta` | Atacadão da Treta | **captura** | `map_atacadao.js` | 276 | | `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 | +| `penitenciaria` | Penitenciária da Treta | **captura** | `map_penitenciaria.js` | 561 | **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. diff --git a/SCRIPTS.md b/SCRIPTS.md index 4204e278..39916391 100644 --- a/SCRIPTS.md +++ b/SCRIPTS.md @@ -138,6 +138,14 @@ Contrato jogável da penitenciária: celas abertas, pátio sem campo, arsenal ce npm run eval:penitenciaria ``` +## `eval:penitenciariavida` + +A Penitenciária da Treta não pode parecer low poly (frente USANTOS, dono 25/08/2026: "os mapas do usantos são os mais low poly do jogo"). NV1 reboco descascado medido nos pixels da DataTexture aplicada nos 4 muros (desvio de luminância, baldes de cor, gradiente de umidade), NV2 4 holofotes `penitenciaria-holofote-*` que VARREM o pátio no `update`, NV3 ≥2 varandas `penitenciaria-varanda-*` com guarda-corpo, NV4 arame farpado em hélice contínua nomeada nos 4 lados, NV5 horizonte próprio (skyUrl + LOOK.penitenciaria), NV6 ≥12 texturas distintas em uso. Irmã da `eval:parquevida`. Mutantes: sem-reboco|holofote-parado|sem-varanda. + +```bash +npm run eval:penitenciariavida +``` + ## `eval:vminspect` O default do vm-inspect precisa ser uma arma publicada, existente e versionada. Nasceu do #141: o caminho antigo dava 404; a primeira correção apontava para o corpo do doador e mostrava pernas/botas. --mutante=fantasma|corpo prova as duas cláusulas. diff --git a/STATUS.md b/STATUS.md index 8480b985..8ef1eb48 100644 --- a/STATUS.md +++ b/STATUS.md @@ -4,8 +4,8 @@ - **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 +- **Código do jogo:** 41.468 linhas em 59 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..13a0e5fd 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.468 linhas em 59 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..d68a8eba 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)` @@ -264,7 +264,7 @@ O registro, gerado do `MAPS` de `public/js/maps.js`: | `atacadao_treta` | Atacadão da Treta | **captura** | `map_atacadao.js` | 276 | | `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 | +| `penitenciaria` | Penitenciária da Treta | **captura** | `map_penitenciaria.js` | 561 | **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. diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index 66039d5b..9cc25c29 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -39,17 +39,17 @@ esta página envelhecia no primeiro commit — ver | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 41.159 linhas em 59 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| Código do jogo | 41.468 linhas em 59 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` | | GLBs de personagem | 63 | `git ls-files 'public/models/characters/*.glb' \| wc -l` | -| Props em GLB | 151 | `git ls-files 'public/models/props/*.glb' \| wc -l` | +| Props em GLB | 153 | `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` | | 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) | @@ -237,7 +237,7 @@ Os mapas registrados hoje, e em que modo cada um abre: | `atacadao_treta` | Atacadão da Treta | **captura** | `map_atacadao.js` | 276 | | `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 | +| `penitenciaria` | Penitenciária da Treta | **captura** | `map_penitenciaria.js` | 561 | **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. @@ -280,10 +280,10 @@ E os dois quality gates, com a lista exata do que cada um roda — direto do `pa {/* BEGIN:GERADO:scripts — não edite à mão, rode `npm run docs` */} ```bash -npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval: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 +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:penitenciariavida 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..546fd23c 100644 --- a/docs/docs/stack.md +++ b/docs/docs/stack.md @@ -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 **67 assets** gerados via Mint (19 `openrouter-image-concept` · 2 `openrouter-concept-tripo-blender-model` · 5 `imagegen-concept` · 32 `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. 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..8e2e42f6 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,468 lines in 59 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..138a5c07 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)` @@ -267,7 +267,7 @@ The registry, generated from the `MAPS` of `public/js/maps.js`: | `atacadao_treta` | Atacadão da Treta | **capture** | `map_atacadao.js` | 276 | | `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 | +| `penitenciaria` | Penitenciária da Treta | **capture** | `map_penitenciaria.js` | 561 | **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. 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..77d56551 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md @@ -41,17 +41,17 @@ this page was aging at the very first commit — see | What | How much | Where to check | |---|---:|---| -| Game code | 41,159 lines in 59 files | `git ls-files public/js/*.js \| xargs wc -l` | +| Game code | 41,468 lines in 59 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` | | Character GLBs | 63 | `git ls-files 'public/models/characters/*.glb' \| wc -l` | -| Props in GLB | 151 | `git ls-files 'public/models/props/*.glb' \| wc -l` | +| Props in GLB | 153 | `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` | | 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) | @@ -239,7 +239,7 @@ The maps registered today, and which mode each one opens in: | `atacadao_treta` | Atacadão da Treta | **capture** | `map_atacadao.js` | 276 | | `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 | +| `penitenciaria` | Penitenciária da Treta | **capture** | `map_penitenciaria.js` | 561 | **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. @@ -282,10 +282,10 @@ And the two gates, with the exact list of what each one runs — straight from ` {/* BEGIN:GERADO:scripts — não edite à mão, rode `npm run docs` */} ```bash -npm run check:fast # node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval: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 +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:penitenciariavida 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..dbafa6cc 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/stack.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/stack.md @@ -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 **67 assets** generated through Mint (19 `openrouter-image-concept` · 2 `openrouter-concept-tripo-blender-model` · 5 `imagegen-concept` · 32 `mint-model` · 2 `tripo3d-task` · 2 `openrouter-concept-meshy-model` · 5 `mint-asset-pack`). 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..808a178b 100644 --- a/mint-assets.json +++ b/mint-assets.json @@ -1883,6 +1883,52 @@ ], "finalSha256": "d5fdc03244bbb0acf4599ea6358b8083a1b6a1f91ce7f9a531c9413ec6052e1c" } + }, + "guarita-vigilancia-penitenciaria": { + "artifactType": "model/gltf-binary", + "files": [ + "public/models/props/penitenciaria_guarita.glb" + ], + "source": { + "kind": "mint-model", + "assetId": "ks7e9bea6wa7jsvkkhzm8rpy998d5w2v", + "chatUrl": "https://mint.gg/chat/ph7dbfdzwpfa6mzdh15wx5tprx8d4c3v", + "displayName": "Weathered Searchlight Guarita (torre de vigilância)", + "frente": "map2-penitenciaria", + "notes": "Torre de vigilância com escada, cabine envidraçada e holofote (4.749 tris). Instanciada nas 4 guaritas do rebuild USANTOS da penitenciaria. GLB normalizado ~1 m, pivô central; escala no call-site (targetH 9,6 / targetLen 5,6)." + }, + "processing": { + "provider": "mint", + "model": "mint text-to-3d (normalized original_glb)", + "tools": [ + "mint.gg", + "tools/optimize-props.mjs" + ], + "finalSha256": "8b03c547171285a460f87c4e3b35213b08e8987ac1052ceb5feb7f467074c4ec" + } + }, + "torre-vigilancia-penitenciaria": { + "artifactType": "model/gltf-binary", + "files": [ + "public/models/props/torre_vigilancia.glb" + ], + "source": { + "kind": "mint-model", + "assetId": "ks79j7c9xgqs48qz8r4cf6gf6d8d6398", + "chatUrl": "https://mint.gg/chat/ph73cszyryt8jreekq64vj4s1x8d66ef", + "displayName": "Prison Guard Tower (torre_vigilancia)", + "frente": "map2-penitenciaria", + "notes": "Torre de vigilância com pernas de concreto em treliça X, escada, varanda com guarda-corpo e cabine envidraçada (4.535 tris). Substitui o visual das 4 guaritas de canto do muro (USANTOS-R2); a guarita GLB anterior segue no acervo. GLB normalizado ~1 m, pivô central; escala no call-site (targetH 9,6 / targetLen 5,6)." + }, + "processing": { + "provider": "mint", + "model": "mint text-to-3d (normalized original_glb)", + "tools": [ + "mint.gg", + "tools/optimize-props.mjs" + ], + "finalSha256": "45943077f7b998cbc3daa5ab5ef6788a0e7483999b2ef161ec126c45128ac31d" + } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 7d705e10..6bb41b9d 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "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:penitenciariavida": "node tools/eval/penitenciaria-vida-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", @@ -178,7 +179,7 @@ "skills:sync": "node tools/sync-skills.mjs", "skills:check": "node tools/sync-skills.mjs --check", "check:deploy": "node tools/eval/runner.mjs syntax eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vmlabhud eval:vminspect eval:redesign eval:charvoice eval:screenquery arch:check docs:check media:check menuwalls:check anims:check anims:merge:check travessao:check eval:medianet eval:posters eval:mutcega eval:autofix eval:deploygate eval:portaointeiro eval:wfsecret eval:comentario eval:fixture eval:preload eval:docsautoria eval:replaycam", - "check:fast": "node tools/eval/runner.mjs syntax eval:release eval:telemetry eval:identity eval:error-console eval:error-origin eval:webgl eval:webglguard eval:maprotate eval:shaderlog eval:shaderbudget eval:botbrain eval:prune eval:vminspect eval:faccao eval:mapid eval:mapjson eval:mapcontrato eval: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", + "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:penitenciariavida 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", "eval:tags": "node tools/eval/tag-main-check.mjs", "eval:simclock": "node tools/eval/sim-clock-check.mjs", "eval:deps": "node tools/eval/deps-check.mjs", diff --git a/public/img/textures/sky_penitenciaria.webp b/public/img/textures/sky_penitenciaria.webp new file mode 100644 index 00000000..8e335920 Binary files /dev/null and b/public/img/textures/sky_penitenciaria.webp differ diff --git a/public/js/look.js b/public/js/look.js index e5562a14..5ba38f98 100644 --- a/public/js/look.js +++ b/public/js/look.js @@ -28,4 +28,16 @@ export const LOOK = { neblina: { d: 0.0082, solDir: [28, 38, 18], forca: 0.84 }, grade: { exposicao: 1.60, piso: 0.0048, expAces: 1.70 }, }, + penitenciaria: { // fim de tarde de presídio: azul-chumbo, glow de vapor de sódio; os holofotes quentes carregam a cena + sky: '/img/textures/sky_penitenciaria.webp', + horizonte: 0x567186, // look-horizonte.py sobre sky_penitenciaria.webp (banda 498-510 de 1024) + zenite: 0x5e85a3, + sol: { cor: 0x9db8d8, i: 0.55, pos: [25, 40, -18] }, + hemi: { ceu: 0x5e7f9d, chao: 0x2a2b26, i: 0.5 }, + neblina: { d: 0.0085, solDir: [25, 40, -18], forca: 0.6 }, + /* grade calibrada por medição (25/08): com 1,50 a média dos 4 frames 3:2 media + L* 20,1 — buraco preto. Alvo noturno L* ≈ 37 (mapa de dia mira 42-48); + piso·exposicao ≈ 0,022 segura blk < 1 % (mesma receita do loja_h R9). */ + grade: { exposicao: 2.60, piso: 0.0085, expAces: 2.80 }, + }, }; diff --git a/public/js/map_penitenciaria.js b/public/js/map_penitenciaria.js index 421ca17f..0bf19500 100644 --- a/public/js/map_penitenciaria.js +++ b/public/js/map_penitenciaria.js @@ -1,12 +1,19 @@ // Penitenciária da Treta: pátio central exposto, celas transitáveis e flancos de serviço. +// Rebuild USANTOS (25/08/2026): reboco, holofotes, varandas, hélice de arame — ver eval:penitenciariavida. import * as THREE from 'three'; import { createFavelaAmbience } from './ambientlife.js'; import { AMB_LOOPS } from './soundscape.js'; +import { applyLook } from './map_sky.js'; +import { placeProp } from './mapprops.js'; const HALF_X = 38; const HALF_Z = 48; -export function buildPenitenciaria(scene) { +/* Subset da fauna que o main.js pré-carrega para este mapa (maps.js ambience). */ +export const PENITENCIARIA_AMBIENCE = ['rat', 'pigeonGround']; +export const PENITENCIARIA_PROPS = ['torre_vigilancia']; + +export function buildPenitenciaria(scene, T) { const root = new THREE.Group(); root.name = 'penitenciaria-da-treta'; scene.add(root); @@ -22,6 +29,101 @@ export function buildPenitenciaria(scene) { if (!geometryCache.has(key)) geometryCache.set(key, new THREE.CylinderGeometry(r, r, h, segments)); return geometryCache.get(key); }; + + /* Texturas medidas em node: DataTexture pixel-a-pixel (idioma texProcedural da + mansão). Canvas é stub no harness — textura que a régua mede nasce aqui. */ + const lcg = (seed) => () => ((seed = (seed * 1664525 + 1013904223) >>> 0) / 4294967296); + function valueNoise(S, cells, seed) { + const rand = lcg(seed), g = []; + for (let y = 0; y <= cells; y++) { g[y] = []; for (let x = 0; x <= cells; x++) g[y][x] = rand(); } + for (let i = 0; i <= cells; i++) { g[i][cells] = g[i][0]; g[cells][i] = g[0][i]; } + return (x, y) => { + const fx = (x / S) * cells, fy = (y / S) * cells; + const ix = Math.min(cells - 1, Math.floor(fx)), iy = Math.min(cells - 1, Math.floor(fy)); + const tx = fx - ix, ty = fy - iy, sx = tx * tx * (3 - 2 * tx), sy = ty * ty * (3 - 2 * ty); + const a = g[iy][ix], b = g[iy][ix + 1], c = g[iy + 1][ix], d = g[iy + 1][ix + 1]; + return a + (b - a) * sx + (c - a) * sy + (a - b - c + d) * sx * sy; + }; + } + function dataTex(name, S, fn, repeatX = 1, repeatY = 1) { + const data = new Uint8Array(S * S * 4); + for (let y = 0; y < S; y++) for (let x = 0; x < S; x++) data.set([...fn(x, y), 255], (y * S + x) * 4); + const t = new THREE.DataTexture(data, S, S, THREE.RGBAFormat); + t.name = name; t.colorSpace = THREE.SRGBColorSpace; + t.wrapS = t.wrapT = THREE.RepeatWrapping; t.repeat.set(repeatX, repeatY); + t.anisotropy = 8; t.needsUpdate = true; return t; + } + /* Reboco creme descascando: manchas de value-noise abrem o concreto e, nas fundas, + o tijolo; borda escura no contorno da placa; fiadas de fôrma a cada 32 px; + umidade escurecendo a base (faixa y<64 — a NV1 mede o gradiente). */ + function texturaReboco() { + const S = 256, mancha = valueNoise(S, 6, 1977), trecho = valueNoise(S, 3, 5551), grao = valueNoise(S, 28, 3313); + return dataTex('penitenciaria-reboco-descascado', S, (x, y) => { + /* trecho (oitava grossa) desloca o limiar: uns panos do muro descascam + quase inteiros, outros ficam quase intactos — muro uniforme é low poly. */ + const n = mancha(x, y) + (trecho(x, y) - .5) * .22, g = (grao(x, y) - .5) * 18; + let r, gg, b; + if (n > .68) { // tijolo aparente no fundo da placa caída + const linha = y % 8 < 1, desloca = (Math.floor(y / 8) % 2) * 6; + const junta = linha || (x + desloca) % 13 < 1; + const t = junta ? 148 : 118 + g; + r = t; gg = junta ? 140 : t * .66; b = junta ? 128 : t * .5; + } else if (n > .575) { // concreto do substrato + const c = 146 + g * 1.2; r = c; gg = c - 5; b = c - 13; + } else if (n > .54) { // sombra da borda da placa de reboco + const c = 118 + g; r = c; gg = c - 4; b = c - 12; + } else { // reboco creme + const c = 204 + g; r = c; gg = c - 7; b = c - 24; + } + if (y % 32 < 2 && n <= .54) { r -= 16; gg -= 16; b -= 14; } // fiada de fôrma + if (y < 64) { // umidade subindo da base + const u = (64 - y) / 64, f = 1 - .34 * u; + r = r * f - 6 * u; gg *= f; b = b * f - 10 * u; + } + return [r, gg, b].map((v) => Math.max(0, Math.min(255, Math.round(v)))); + }, 12, 1); + } + const texturaGalvanizado = () => dataTex('penitenciaria-metal-galvanizado', 128, (x, y) => { + const vago = valueNoise(128, 10, 771)(x, y), c = 148 + (vago - .5) * 26 + (x % 16 < 1 ? -22 : 0); + return [c - 4, c, c + 4].map((v) => Math.max(0, Math.min(255, Math.round(v)))); + }, 4, 4); + const texturaArame = () => dataTex('penitenciaria-arame-farpado', 64, (x, y) => { + const farpa = (x + y * 2) % 16 < 3, c = farpa ? 132 : 74 + ((x * 7 + y * 13) % 9) * 3; + return [c, c * .62, c * .42].map((v) => Math.max(0, Math.min(255, Math.round(v)))); + }, 160, 2); + const texturaGrade = () => dataTex('penitenciaria-grade-cela', 128, (x, y) => { + const barra = x % 16 < 3 || y % 42 < 4, c = barra ? 128 + ((x + y) % 5) * 4 : 16 + ((x * 3 + y * 7) % 4) * 2; + return barra ? [c - 6, c, c + 4] : [c, c + 2, c + 4]; + }, 1, 1); + const texturaTijolo = () => dataTex('penitenciaria-tijolo-aparente', 128, (x, y) => { + const linha = y % 16 < 2, desloca = (Math.floor(y / 16) % 2) * 16; + const junta = linha || (x + desloca) % 32 < 2; + const tom = ((Math.floor(x / 32) * 5 + Math.floor(y / 16) * 3) % 5 - 2) * 6 + ((x * 11 + y * 17) % 7) - 3; + return junta ? [142 + tom, 134 + tom, 122 + tom] : [120 + tom, 76 + tom * .7, 58 + tom * .5]; + }, 10, 2); + const texturaPoca = () => dataTex('penitenciaria-poca', 64, (x, y) => { + const c = 22 + valueNoise(64, 8, 99)(x, y) * 14; + return [c * .8, c * .95, c * 1.25].map((v) => Math.round(v)); + }, 1, 1); + const texturaCaixaDagua = () => dataTex('penitenciaria-caixa-dagua', 128, (x, y) => { + const faixa = y % 32 < 3, escorrido = valueNoise(128, 12, 551)(x, y) > .78; + const c = 92 + ((x * 5 + y * 3) % 7) * 2; + return faixa || escorrido ? [c * .8, c * .62, c * .4].map(Math.round) : [c * .75, c * .85, c * .95].map(Math.round); + }, 2, 1); + const texturaMesa = () => dataTex('penitenciaria-mesa-inox', 128, (x, y) => { + const c = 156 + (valueNoise(128, 20, 881)(x, y) - .5) * 22 + (y % 64 < 2 ? -30 : 0); + return [c - 6, c - 2, c + 2].map((v) => Math.max(0, Math.min(255, Math.round(v)))); + }, 2, 1); + const texturaPatio = () => { + const S = 256, mancha = valueNoise(S, 5, 4242), grao = valueNoise(S, 30, 777); + return dataTex('penitenciaria-patio-concreto-gasto', S, (x, y) => { + const junta = x % 64 < 2 || y % 64 < 2; + let c = junta ? 52 : 84 + (grao(x, y) - .5) * 20; + if (mancha(x, y) > .68) c -= 14; + return [c, c + 2, c].map((v) => Math.max(0, Math.min(255, Math.round(v)))); + }, 8, 8); + }; + function proceduralTexture(name, base, detail, mode, repeatX = 4, repeatY = repeatX) { const canvas = document.createElement('canvas'); canvas.width = canvas.height = 128; const ctx = canvas.getContext('2d'); ctx.fillStyle = base; ctx.fillRect(0, 0, 128, 128); @@ -65,9 +167,17 @@ export function buildPenitenciaria(scene) { const tex = { concrete: proceduralTexture('penitenciaria-concreto', '#777b78', '#343936', 'concrete', 6), darkConcrete: proceduralTexture('penitenciaria-concreto-escuro', '#343a3b', '#15191a', 'concrete', 5), - yard: proceduralTexture('penitenciaria-patio-concreto-gasto', '#555957', '#262a29', 'concrete', 8), + yard: texturaPatio(), steel: proceduralTexture('penitenciaria-aco-enferrujado', '#565d5e', '#8b6b49', 'metal', 3), ammo: ammoCrateTexture(), + reboco: texturaReboco(), + galvanizado: texturaGalvanizado(), + arame: texturaArame(), + grade: texturaGrade(), + tijolo: texturaTijolo(), + poca: texturaPoca(), + caixaDagua: texturaCaixaDagua(), + mesa: texturaMesa(), }; const MAT = { concrete: new THREE.MeshStandardMaterial({ map: tex.concrete, bumpMap: tex.concrete, bumpScale: .045, color: 0xb8bbb5, roughness: .93 }), @@ -84,6 +194,14 @@ export function buildPenitenciaria(scene) { rubber: new THREE.MeshStandardMaterial({ color: 0x17191a, roughness: .96 }), grass: new THREE.MeshStandardMaterial({ color: 0x52643c, roughness: 1 }), ammo: new THREE.MeshStandardMaterial({ map: tex.ammo, bumpMap: tex.ammo, bumpScale: .035, color: 0xffffff, roughness: .76, metalness: .18 }), + reboco: new THREE.MeshStandardMaterial({ map: tex.reboco, bumpMap: tex.reboco, bumpScale: .06, color: 0xcfc8ba, roughness: .9 }), + galvanizado: new THREE.MeshStandardMaterial({ map: tex.galvanizado, bumpMap: tex.galvanizado, bumpScale: .02, color: 0xb9c0c4, metalness: .68, roughness: .44 }), + arame: new THREE.MeshStandardMaterial({ map: tex.arame, color: 0x9a7a5c, metalness: .55, roughness: .6 }), + grade: new THREE.MeshStandardMaterial({ map: tex.grade, color: 0xaab2b6, metalness: .6, roughness: .5 }), + tijolo: new THREE.MeshStandardMaterial({ map: tex.tijolo, bumpMap: tex.tijolo, bumpScale: .05, color: 0xbd9e88, roughness: .92 }), + poca: new THREE.MeshStandardMaterial({ map: tex.poca, color: 0x39434e, metalness: .4, roughness: .07 }), + caixaDagua: new THREE.MeshStandardMaterial({ map: tex.caixaDagua, bumpMap: tex.caixaDagua, bumpScale: .03, color: 0x9fb2bd, metalness: .35, roughness: .6 }), + mesa: new THREE.MeshStandardMaterial({ map: tex.mesa, bumpMap: tex.mesa, bumpScale: .015, color: 0xc4c9cc, metalness: .7, roughness: .38 }), }; function addBox(w, h, d, material, x, y, z, opts = {}) { const mesh = new THREE.Mesh(boxGeo(w, h, d), material); mesh.position.set(x, y + h / 2, z); @@ -103,70 +221,201 @@ export function buildPenitenciaria(scene) { if (opts.collide) { const collider = { minX: x-r, maxX: x+r, minY: y, maxY: y+h, minZ: z-r, maxZ: z+r, tag: opts.tag }; colliders.push(collider); mesh.userData.collider = collider; } return mesh; } + /* Caixas repetidas (barras de cela, postes, mísulas): 1 InstancedMesh por + (dimensão,material) em vez de 1 mesh por peça — era o que inflava o mapa + para 1.167 meshes. Collider e occluder seguem a mesma regra do addBox. */ + const instGroups = new Map(); + function ibox(w, h, d, material, x, y, z, opts = {}) { + const key = `${w}|${h}|${d}|${material.uuid}|${opts.cast === false ? 0 : 1}`; + let g = instGroups.get(key); + if (!g) { g = { geo: boxGeo(w, h, d), mat: material, list: [], cast: opts.cast !== false, occluder: false }; instGroups.set(key, g); } + const o = new THREE.Object3D(); o.position.set(x, y + h / 2, z); + if (opts.ry) o.rotation.y = opts.ry; o.updateMatrix(); + g.list.push(o.matrix.clone()); + if (opts.collide) { + const hx = Math.abs(Math.cos(opts.ry || 0)) * w / 2 + Math.abs(Math.sin(opts.ry || 0)) * d / 2; + const hz = Math.abs(Math.sin(opts.ry || 0)) * w / 2 + Math.abs(Math.cos(opts.ry || 0)) * d / 2; + colliders.push({ minX: x - hx, maxX: x + hx, minY: y, maxY: y + h, minZ: z - hz, maxZ: z + hz, tag: opts.tag }); + if (h > 1.2) g.occluder = true; + } + } + function buildInstanced() { + for (const g of instGroups.values()) { + const im = new THREE.InstancedMesh(g.geo, g.mat, g.list.length); + g.list.forEach((m, i) => im.setMatrixAt(i, m)); + im.castShadow = g.cast; im.receiveShadow = true; + im.instanceMatrix.needsUpdate = true; im.computeBoundingSphere(); + root.add(im); + if (g.occluder) occluders.push(im); + } + instGroups.clear(); + } + + /* Look de fim de tarde azul-chumbo (LOOK.penitenciaria): céu/fog/sol/hemi de uma + fonte só; o shadow fica no builder porque ele conhece os limites do mapa. */ + const { hemi, sun } = applyLook(scene, T, 'penitenciaria'); + sun.shadow.mapSize.set(2048, 2048); + sun.shadow.camera.left = -50; sun.shadow.camera.right = 50; + sun.shadow.camera.top = 58; sun.shadow.camera.bottom = -58; + sun.shadow.camera.far = 180; sun.shadow.bias = -.0004; - scene.background = new THREE.Color(0xa8b5b7); scene.fog = new THREE.Fog(0x9aa7a8, 82, 165); const ground = new THREE.Mesh(new THREE.PlaneGeometry(150, 175), MAT.darkConcrete); ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true; root.add(ground); - // Muro maciço impede fuga; a tela metálica e o arame farpado dão a leitura prisional. - addBox(76, 5.8, 1, MAT.concrete, 0, 0, -HALF_Z, { tag: 'muro-sul' }); - addBox(76, 5.8, 1, MAT.concrete, 0, 0, HALF_Z, { tag: 'muro-norte' }); - addBox(1, 5.8, 96, MAT.concrete, -HALF_X, 0, 0, { tag: 'muro-oeste' }); - addBox(1, 5.8, 96, MAT.concrete, HALF_X, 0, 0, { tag: 'muro-leste' }); + /* Muro de reboco descascado sobre embasamento de concreto: a faixa escura de + umidade da textura encontra o embasamento físico — a junção reboco↔concreto + é lida nos dois níveis. Platibanda escura fecha o topo. */ + const MURO_H = 5.8; + for (const [w, d, x, z, tag] of [ + [76, 1, 0, -HALF_Z, 'muro-sul'], [76, 1, 0, HALF_Z, 'muro-norte'], + [1, 96, -HALF_X, 0, 'muro-oeste'], [1, 96, HALF_X, 0, 'muro-leste'], + ]) { + addBox(w, MURO_H, d, MAT.reboco, x, 0, z, { tag }); + addBox(w === 1 ? 1.3 : w + .4, .9, d === 1 ? 1.3 : d + .4, MAT.darkConcrete, x, 0, z, { collide: false, cast: false }); + addBox(w === 1 ? 1.4 : w + .5, .3, d === 1 ? 1.4 : d + .5, MAT.darkConcrete, x, MURO_H, z, { collide: false }); + } function fence(name, axis, fixed, from, to) { const group = new THREE.Group(); group.name = `penitenciaria-cerca-${name}`; root.add(group); for (let p = from; p <= to; p += 3) { - const mesh = axis === 'x' ? addBox(.08, 2.5, .08, MAT.steel, p, 5.8, fixed, { collide: false }) : addBox(.08, 2.5, .08, MAT.steel, fixed, 5.8, p, { collide: false }); group.add(mesh); root.remove(mesh); + axis === 'x' ? ibox(.08, 2.5, .08, MAT.steel, p, MURO_H, fixed) : ibox(.08, 2.5, .08, MAT.steel, fixed, MURO_H, p); } for (const y of [6.2, 7.25, 8.1]) { - const rail = axis === 'x' ? addBox(to-from, .06, .06, MAT.steel, (from+to)/2, y, fixed, { collide: false }) : addBox(.06, .06, to-from, MAT.steel, fixed, y, (from+to)/2, { collide: false }); group.add(rail); root.remove(rail); + axis === 'x' ? ibox(to - from, .06, .06, MAT.steel, (from + to) / 2, y, fixed) : ibox(.06, .06, to - from, MAT.steel, fixed, y, (from + to) / 2); } - const wire = new THREE.Mesh(new THREE.TorusGeometry(.34, .025, 4, 10), MAT.rust); wire.rotation.y = axis === 'x' ? Math.PI / 2 : 0; - for (let p = from + .5; p < to; p += .7) { const coil = wire.clone(); axis === 'x' ? coil.position.set(p, 8.45, fixed) : coil.position.set(fixed, 8.45, p); group.add(coil); } } fence('sul', 'x', -HALF_Z, -HALF_X, HALF_X); fence('norte', 'x', HALF_Z, -HALF_X, HALF_X); fence('oeste', 'z', -HALF_X, -HALF_Z, HALF_Z); fence('leste', 'z', HALF_X, -HALF_Z, HALF_Z); + /* Arame farpado em hélice contínua (NV4): um TubeGeometry por lado no lugar dos + 432 torus clonados. A curva é local ao mesh, que fica posicionado no lado. */ + class HelixCurve extends THREE.Curve { + constructor(axis, from, to, r, pitch) { super(); this.axis = axis; this.from = from; this.to = to; this.r = r; this.pitch = pitch; } + getPoint(t, target = new THREE.Vector3()) { + const p = this.from + (this.to - this.from) * t; + const a = ((p - this.from) / this.pitch) * Math.PI * 2; + return this.axis === 'x' ? target.set(p, this.r * Math.cos(a), this.r * Math.sin(a)) : target.set(this.r * Math.sin(a), this.r * Math.cos(a), p); + } + } + for (const [name, axis, mx, mz, from, to] of [ + ['sul', 'x', 0, -HALF_Z, -HALF_X, HALF_X], ['norte', 'x', 0, HALF_Z, -HALF_X, HALF_X], + ['oeste', 'z', -HALF_X, 0, -HALF_Z, HALF_Z], ['leste', 'z', HALF_X, 0, -HALF_Z, HALF_Z], + ]) { + const turns = (to - from) / .7; + const tube = new THREE.Mesh(new THREE.TubeGeometry(new HelixCurve(axis, from, to, .34, .7), Math.ceil(turns) * 8, .028, 4, false), MAT.arame); + tube.name = `penitenciaria-arame-${name}`; + tube.position.set(mx, 8.45, mz); tube.castShadow = false; root.add(tube); + } + + /* Guaritas com holofote REAL que varre o pátio (NV2). SpotLight SEM sombra e sem + .map: o SB2 já mede 8/8 no piso WebGL1 (mutantes sombra-pontual/spot-map provam). */ + const holofotes = []; + const coneGeo = new THREE.ConeGeometry(3.4, 30, 12, 1, true); + coneGeo.translate(0, -15, 0); coneGeo.rotateX(-Math.PI / 2); // ápice na origem, boca a +Z + const coneMat = new THREE.MeshBasicMaterial({ color: 0xffc07a, transparent: true, opacity: .055, blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide, fog: false }); function guardTower(index, x, z) { const group = new THREE.Group(); group.name = `penitenciaria-guarita-${index}`; root.add(group); + group.userData.molde = 'torre_vigilancia'; // NV7: a régua lê o USO registrado, não a declaração const sx = Math.sign(x), sz = Math.sign(z); - for (const dx of [-1.8, 1.8]) for (const dz of [-1.8, 1.8]) addBox(.38, 7.2, .38, MAT.steel, x+dx, 0, z+dz, { tag: `guarita-${index}` }); - addBox(4.8, .45, 4.8, MAT.concrete, x, 6.5, z, { tag: `guarita-${index}` }); - addBox(4.2, 2.4, .25, MAT.steel, x, 6.95, z-sz*2, { tag: `guarita-${index}` }); - addBox(.25, 2.4, 4.2, MAT.steel, x-sx*2, 6.95, z, { tag: `guarita-${index}` }); - addBox(4.8, .4, 4.8, MAT.darkConcrete, x, 9.35, z, { collide: false }); - for (const side of [-1, 1]) addBox(.08, 5.8, .08, MAT.steel, x+sx*(2.25+side*.35), .2, z-sz*2.2, { collide: false }); + /* GLB da torre (Mint, FONTE.md): no arnês node placeProp devolve null e a + torre procedural cobre — colisores idênticos nos dois mundos (lição 3). */ + const glb = placeProp('torre_vigilancia', { x, y: 0, z, targetH: 9.6, targetLen: 5.6, ry: Math.atan2(-x, -z) }); + if (glb) root.add(glb); + const pecas = []; + for (const dx of [-1.8, 1.8]) for (const dz of [-1.8, 1.8]) pecas.push(addBox(.38, 7.2, .38, MAT.steel, x+dx, 0, z+dz, { tag: `guarita-${index}` })); + pecas.push(addBox(4.8, .45, 4.8, MAT.concrete, x, 6.5, z, { tag: `guarita-${index}` })); + pecas.push(addBox(4.2, 2.4, .25, MAT.steel, x, 6.95, z-sz*2, { tag: `guarita-${index}` })); + pecas.push(addBox(.25, 2.4, 4.2, MAT.steel, x-sx*2, 6.95, z, { tag: `guarita-${index}` })); + pecas.push(addBox(4.8, .4, 4.8, MAT.darkConcrete, x, 9.35, z, { collide: false })); + for (const side of [-1, 1]) pecas.push(addBox(.08, 5.8, .08, MAT.steel, x+sx*(2.25+side*.35), .2, z-sz*2.2, { collide: false })); + if (glb) for (const p of pecas) { p.visible = false; const o = occluders.indexOf(p); if (o >= 0) occluders.splice(o, 1); } + + const cabeca = new THREE.Group(); cabeca.name = `penitenciaria-holofote-${index}`; + cabeca.position.set(x - sx * .8, 8.6, z - sz * .8); root.add(cabeca); + const corpo = new THREE.Mesh(boxGeo(.5, .42, .62), MAT.black); corpo.castShadow = false; cabeca.add(corpo); + const lente = new THREE.Mesh(new THREE.CircleGeometry(.2, 12), new THREE.MeshBasicMaterial({ color: 0xffd9a0, fog: false })); + lente.position.z = .32; cabeca.add(lente); + const spot = new THREE.SpotLight(0xffc07a, 2.6, 85, .34, .5, 0); + cabeca.add(spot); + const alvo = new THREE.Object3D(); root.add(alvo); spot.target = alvo; + const cone = new THREE.Mesh(coneGeo, coneMat); cone.renderOrder = 9; cabeca.add(cone); + holofotes.push({ cabeca, alvo, cone, fase: index * Math.PI * .5, giro: index % 2 ? 1 : -1, cx: sx * 9, cz: sz * 11 }); } guardTower(0, -33.5, -43.5); guardTower(1, 33.5, -43.5); guardTower(2, -33.5, 43.5); guardTower(3, 33.5, 43.5); + /* Celas do térreo (contrato PEN1/PEN2 intacto) + beliche no fundo e porta de + grade entreaberta. Estrutura repetida sai instanciada; o que tem nome de + contrato continua mesh próprio. */ function cell(side, index, z) { const faceX = side * 25, backX = side * 34.2, insideX = side * 29.3; const group = new THREE.Group(); group.name = `penitenciaria-cela-aberta-${side < 0 ? 'o' : 'l'}-${index}`; group.userData = { doorwayX: side * 24.8, doorwayZ: z, insideX, insideZ: z }; root.add(group); - addBox(9.2, .35, 7.2, MAT.concrete, (faceX+backX)/2, 4.1, z, { collide: false }); - addBox(.5, 4.1, 7.2, MAT.concrete, backX, 0, z); - addBox(9.2, 4.1, .42, MAT.concrete, (faceX+backX)/2, 0, z-3.6); - addBox(9.2, 4.1, .42, MAT.concrete, (faceX+backX)/2, 0, z+3.6); + ibox(9.2, .35, 7.2, MAT.concrete, (faceX+backX)/2, 4.1, z); + ibox(.5, 4.1, 7.2, MAT.concrete, backX, 0, z, { collide: true }); + ibox(9.2, 4.1, .42, MAT.concrete, (faceX+backX)/2, 0, z-3.6, { collide: true }); + ibox(9.2, 4.1, .42, MAT.concrete, (faceX+backX)/2, 0, z+3.6, { collide: true }); const barX = faceX; - for (const dz of [-3.25,-2.7,-2.15,2.15,2.7,3.25]) addBox(.12, 3.85, .12, MAT.steel, barX, 0, z+dz, { collide: false }); - for (const y of [.6,2,3.35]) { addBox(.12, .1, 2.9, MAT.steel, barX, y, z-2.15, { collide: false }); addBox(.12, .1, 2.9, MAT.steel, barX, y, z+2.15, { collide: false }); } + for (const dz of [-3.25,-2.7,-2.15,2.15,2.7,3.25]) ibox(.12, 3.85, .12, MAT.steel, barX, 0, z+dz); + for (const y of [.6,2,3.35]) { ibox(.12, .1, 2.9, MAT.steel, barX, y, z-2.15); ibox(.12, .1, 2.9, MAT.steel, barX, y, z+2.15); } addBox(.45, 1.1, 2.6, MAT.concrete, side*31.6, 0, z, { name: `penitenciaria-banco-${side}-${index}` }); - addBox(.1, .08, 1.5, MAT.white, backX-side*.27, 2.1, z, { collide: false }); + ibox(.1, .08, 1.5, MAT.white, backX-side*.27, 2.1, z); + // porta de grade entreaberta, encostada na ombreira (decorativa: a passagem fica livre) + ibox(.04, 3.3, 1.35, MAT.grade, barX - side * .5, 0, z - 2.85, { ry: side * .5, cast: false }); + // beliche no fundo da cela: cala o canto morto sem tocar porta nem centro (PEN2) + const bx = side * 32.7, bz = z - 2.1; + for (const px of [-.42, .42]) for (const pz of [-.95, .95]) ibox(.08, 1.65, .08, MAT.galvanizado, bx + px, 0, bz + pz); + for (const by of [.45, 1.35]) { ibox(.95, .07, 2.05, MAT.galvanizado, bx, by, bz); ibox(.85, .12, 1.9, MAT.grass, bx, by + .07, bz); } + colliders.push({ minX: bx - .5, maxX: bx + .5, minY: 0, maxY: 1.7, minZ: bz - 1.05, maxZ: bz + 1.05, tag: 'beliche' }); } [-30,-20,-10,10,20,30].forEach((z, i) => { cell(-1, i, z); cell(1, i, z); }); + /* 1º pavimento das duas alas (NV3): pavimento superior em tijolo aparente com + portas de grade para a passarela, varanda metálica galvanizada com + guarda-corpo e escada de acesso visual na ponta sul. */ + for (const side of [-1, 1]) { + const ala = side < 0 ? 'o' : 'l'; + addBox(9.2, 4.15, 70.4, MAT.tijolo, side * 29.9, 4.45, 0); + addBox(9.6, .35, 71.2, MAT.darkConcrete, side * 29.9, 8.6, 0, { collide: false }); + for (const z of [-30,-20,-10,10,20,30]) { + ibox(.3, 2.3, 1.5, MAT.black, side * 25.15, 4.6, z, { cast: false }); + ibox(.05, 2.2, 1.3, MAT.grade, side * 25.05, 4.65, z, { cast: false }); + ibox(.05, .9, .9, MAT.grade, side * 25.05, 5.65, z + 5, { cast: false }); + } + const varanda = new THREE.Group(); varanda.name = `penitenciaria-varanda-${ala}`; root.add(varanda); + const piso = new THREE.Mesh(boxGeo(2.4, .18, 70.4), MAT.galvanizado); + piso.position.set(side * 23.55, 4.54, 0); piso.castShadow = true; piso.receiveShadow = true; varanda.add(piso); + for (let z = -33; z <= 33; z += 3.3) { + ibox(.1, .1, 1.6, MAT.galvanizado, side * 24.6, 4.28, z, { ry: 0, cast: false }); // mísula + ibox(.07, 1.1, .07, MAT.galvanizado, side * 22.5, 4.63, z); // balaústre + } + for (const y of [5.15, 5.68]) ibox(.08, .07, 70.4, MAT.galvanizado, side * 22.5, y, 0); + const guardaNome = `penitenciaria-varanda-${ala}-guarda-corpo`; + const tela = new THREE.Mesh(new THREE.PlaneGeometry(70.4, 1.05), MAT.grade); + tela.name = guardaNome; tela.position.set(side * 22.5, 5.15, 0); tela.rotation.y = Math.PI / 2; + tela.castShadow = false; varanda.add(tela); + // escada de acesso visual na ponta sul (decorative: sem colisor novo) + for (let d = 0; d < 10; d++) ibox(2.0, .16, .5, MAT.galvanizado, side * 23.55, d * .46, -36.2 - d * .42, { cast: false }); + ibox(.07, 4.6, .07, MAT.galvanizado, side * 22.6, 0, -36.4); ibox(.07, 4.6, .07, MAT.galvanizado, side * 24.5, 0, -36.4); + } + // Pátio bruto: concreto remendado, manchas de umidade e drenagem, sem marcação esportiva. const yard = new THREE.Mesh(new THREE.PlaneGeometry(35, 43), MAT.yard); yard.name = 'penitenciaria-patio'; yard.rotation.x = -Math.PI/2; yard.position.y = .018; yard.receiveShadow = true; root.add(yard); for (const [x,z,sx,sz] of [[-11,-14,4,1.8],[9,-12,5,2.4],[-13,10,3,5],[11,14,5,2],[-2,17,7,1.4]]) { const stain = new THREE.Mesh(new THREE.CircleGeometry(1,18), new THREE.MeshBasicMaterial({color:0x252c27,transparent:true,opacity:.2,depthWrite:false})); stain.scale.set(sx,sz,1); stain.rotation.x=-Math.PI/2; stain.position.set(x,.032,z); root.add(stain); } - for (const z of [-18,18]) { addBox(29,.055,.22,MAT.steel,0,.02,z,{collide:false,cast:false}); for(let x=-13;x<=13;x+=1.1)addBox(.06,.065,1.1,MAT.black,x,.025,z,{collide:false,cast:false}); } + for (const z of [-18,18]) { ibox(29,.055,.22,MAT.steel,0,.02,z,{cast:false}); for(let x=-13;x<=13;x+=1.1)ibox(.06,.065,1.1,MAT.black,x,.025,z,{cast:false}); } + // juntas de dilatação do piso e poças que devolvem a luz dos holofotes + for (const x of [-14, -7, 0, 7, 14]) ibox(.16, .02, 43, MAT.black, x, .012, 0, { cast: false }); + for (const z of [-14, -7, 0, 7, 14]) ibox(35, .02, .16, MAT.black, 0, .013, z, { cast: false }); + for (const [x, z, r] of [[-6, -9, 2.2], [8, 4, 1.7], [-3, 12, 1.4], [12, -15, 1.2]]) { + const poca = new THREE.Mesh(new THREE.CircleGeometry(r, 22), MAT.poca); + poca.rotation.x = -Math.PI / 2; poca.position.set(x, .026, z); poca.receiveShadow = true; root.add(poca); + } function ammoCrate(index, x, z, ry=0) { const group = new THREE.Group(); group.name = `penitenciaria-caixa-municao-${index}`; root.add(group); const body = addBox(2.2, 1.25, 1.55, MAT.ammo, x, 0, z, { ry, tag: `municao-${index}` }); group.userData.collider = body.userData.collider; - for (const y of [.18,.92]) addBox(2.28,.1,1.63,MAT.steel,x,y,z,{ry,collide:false}); - for (const dx of [-.65,0,.65]) addBox(.08,.7,1.65,MAT.black,x+dx*Math.cos(ry),.27,z-dx*Math.sin(ry),{ry,collide:false}); + for (const y of [.18,.92]) ibox(2.28,.1,1.63,MAT.steel,x,y,z,{ry}); + for (const dx of [-.65,0,.65]) ibox(.08,.7,1.65,MAT.black,x+dx*Math.cos(ry),.27,z-dx*Math.sin(ry),{ry}); } [[-8,-7,.2],[8,-7,-.2],[-8,7,-.15],[8,7,.15],[-16,0,1.57],[16,0,1.57]].forEach((p,i)=>ammoCrate(i,...p)); @@ -220,6 +469,46 @@ export function buildPenitenciaria(scene) { for(const dx of [-1.65,1.65]) addBox(.45,.72,.8,MAT.darkConcrete,x+dx,0,z); }); + /* Refeitório coberto no canto sudoeste: mesa coletiva de inox com bancos — + a cena do dia a dia que tira o pátio do abstrato. */ + { + const rx = -26, rz = -42; + addBox(8.4, .22, 6.2, MAT.darkConcrete, rx, 3.05, rz, { collide: false }); + for (const dx of [-3.8, 3.8]) for (const dz of [-2.7, 2.7]) addBox(.22, 3.05, .22, MAT.galvanizado, rx + dx, 0, rz + dz, { tag: 'refeitorio' }); + for (const dz of [-1.8, 0, 1.8]) { + addBox(4.2, .09, 1.0, MAT.mesa, rx, .78, rz + dz, { tag: 'refeitorio' }); + for (const dx of [-1.7, 1.7]) ibox(.14, .78, .9, MAT.galvanizado, rx + dx, 0, rz + dz, { collide: true }); + for (const s of [-1, 1]) { ibox(4.2, .07, .4, MAT.galvanizado, rx, .45, rz + dz + s * .85); for (const dx of [-1.7, 1.7]) ibox(.12, .45, .35, MAT.galvanizado, rx + dx, 0, rz + dz + s * .85); } + } + } + + /* Caixa d'água de aço sobre cavaletes, rente ao muro leste. */ + { + const cx = 31, cz = -36; + for (const dx of [-1.2, 1.2]) for (const dz of [-1.2, 1.2]) addBox(.18, 3.6, .18, MAT.steel, cx + dx, 0, cz + dz, { tag: 'caixa-dagua' }); + addCylinder(1.7, 2.3, MAT.caixaDagua, cx, 3.6, cz, { segments: 18 }); + addCylinder(1.78, .18, MAT.steel, cx, 5.82, cz, { segments: 18 }); + } + + /* Postes com fios e luminárias quentes: ritmo vertical no pátio vazio. */ + const posteTopo = []; + for (const [x, z] of [[-18, -10], [18, -10], [-18, 10], [18, 10]]) { + addBox(.22, 7, .22, MAT.galvanizado, x, 0, z, { tag: 'poste' }); + addBox(1.6, .12, .12, MAT.galvanizado, x, 6.6, z, { collide: false }); + const lamp = new THREE.Mesh(boxGeo(.5, .18, .3), new THREE.MeshStandardMaterial({ color: 0x2a2d2e, emissive: 0xffb35c, emissiveIntensity: 1.6, roughness: .5 })); + lamp.position.set(x + .55, 6.62, z); root.add(lamp); + posteTopo.push([x, 6.55, z]); + } + for (let i = 0; i < posteTopo.length - 1; i++) { + const [ax, ay, az] = posteTopo[i], [bx, by, bz] = posteTopo[i + 1]; + const sag = Math.hypot(bx - ax, bz - az) * .04; + const curva = new THREE.CatmullRomCurve3([ + new THREE.Vector3(ax, ay, az), new THREE.Vector3((ax + bx) / 2, Math.min(ay, by) - sag, (az + bz) / 2), new THREE.Vector3(bx, by, bz), + ]); + const fio = new THREE.Mesh(new THREE.TubeGeometry(curva, 20, .016, 4, false), MAT.black); + fio.castShadow = false; root.add(fio); + } + const GM={dark:MAT.black,steel:MAT.steel,wood:MAT.rust}; function gun(kind,x,z,yaw){const g=new THREE.Group();g.name=`arma-central-${kind}`;g.position.set(x,.1,z);g.rotation.y=yaw;root.add(g);const long=['awp','ak','m4','shotgun','mp5'].includes(kind);const body=new THREE.Mesh(boxGeo(.13,.13,long?1:.42),kind==='shotgun'?GM.wood:GM.dark);body.position.y=.1;g.add(body);if(long){const barrel=new THREE.Mesh(boxGeo(.08,.08,.55),GM.steel);barrel.position.set(0,.13,-.62);g.add(barrel);}const grip=new THREE.Mesh(boxGeo(.11,.25,.14),GM.wood);grip.position.set(0,-.02,long?.25:.12);g.add(grip);pickups.push({x,z,kind,weapon:kind,readyAt:0,mesh:g});} /* `kind` é ID de arma (chave de WEAPONS), não CLASSE: o 8º era 'smg' e crashava @@ -227,9 +516,17 @@ export function buildPenitenciaria(scene) { ['awp','ak','m4','shotgun','mp5','deagle','pistol','uzi'].forEach((kind,i)=>gun(kind,-10+i*(20/7),i%2?-2.2:2.2,i*.42)); ['ak','m4','shotgun','deagle'].forEach((kind,i)=>{gun(kind,-15+i*10,-41,0);gun(kind,15-i*10,41,Math.PI);}); - const hemi=new THREE.HemisphereLight(0xdbe8eb,0x343a36,1.35);scene.add(hemi); - const sun=new THREE.DirectionalLight(0xffe7c7,1.85);sun.position.set(-35,52,-22);sun.castShadow=true;sun.shadow.mapSize.set(2048,2048);sun.shadow.camera.left=-50;sun.shadow.camera.right=50;sun.shadow.camera.top=58;sun.shadow.camera.bottom=-58;sun.shadow.camera.far=180;sun.shadow.bias=-.0004;scene.add(sun); - const fill=new THREE.DirectionalLight(0x7897ba,.38);fill.position.set(28,24,35);scene.add(fill); + buildInstanced(); + + /* Varredura LENTA dos holofotes (NV2): cada cabeça gira num quadrante do pátio, + ~55 s por volta; o spot real e o cone falso miram o mesmo alvo. */ + function update(dt, time) { + for (const h of holofotes) { + const a = time * .115 * h.giro + h.fase; + h.alvo.position.set(h.cx + Math.cos(a) * 13, 0, h.cz + Math.sin(a) * 15); + h.cabeca.lookAt(h.alvo.position.x, 1.1, h.alvo.position.z); + } + } const groundHeightAt=()=>0, slowAt=()=>false; const bounds={minX:-HALF_X+.9,maxX:HALF_X-.9,minZ:-HALF_Z+.9,maxZ:HALF_Z-.9}; @@ -257,7 +554,7 @@ export function buildPenitenciaria(scene) { }); return { - ambience,sound:{loops:[{src:AMB_LOOPS.vento,pos:[0,3,0],radius:70,vol:.22},{src:AMB_LOOPS.hum,pos:[0,3,0],radius:70,vol:.16}],bioma:'urbano'},root,colliders,occluders,decalSolids:[root],groundHeightAt,slowAt,pickups,sun,hemi, + ambience,sound:{loops:[{src:AMB_LOOPS.vento,pos:[0,3,0],radius:70,vol:.22},{src:AMB_LOOPS.hum,pos:[0,3,0],radius:70,vol:.16}],bioma:'urbano'},root,colliders,occluders,decalSolids:[root],groundHeightAt,slowAt,update,pickups,sun,hemi, spawns:{E:[-15,-5,5,15].map(x=>({x,z:-42,yaw:0})),B:[15,5,-5,-15].map(x=>({x,z:42,yaw:Math.PI}))}, ctfPoints:[{id:'E',label:'ALA SUL',x:0,z:-39},{id:'MID',label:'PÁTIO',x:0,z:0},{id:'B',label:'ALA NORTE',x:0,z:39}], waypoints:{nodes,adj},nearestWaypoint,findPath,bounds}; diff --git a/public/js/maps.js b/public/js/maps.js index cb3b74c6..a898f5b0 100644 --- a/public/js/maps.js +++ b/public/js/maps.js @@ -15,7 +15,7 @@ import { buildUpa, UPA_PROPS } from './map_upa.js'; 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 { buildPenitenciaria, PENITENCIARIA_AMBIENCE, PENITENCIARIA_PROPS } from './map_penitenciaria.js'; /* IDS SEM NOME DE COUNTER-STRIKE (rodada de 11/08). ═══════════════════════════════════════════════════════════════════════════════════ @@ -91,7 +91,7 @@ export const MAPS = { atacadao_treta: { name: 'Atacadão da Treta', build: buildAtacadao, props: ATACADAO_PROPS, ctfMode: true }, 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 }, + penitenciaria: { name: 'Penitenciária da Treta', build: buildPenitenciaria, props: PENITENCIARIA_PROPS, ambience: PENITENCIARIA_AMBIENCE, ctfMode: true }, }; export const MAP_IDS = Object.keys(MAPS); export const DEFAULT_MAP = 'praca_poderes'; diff --git a/public/models/props/FONTE.md b/public/models/props/FONTE.md index 0588565d..3284f957 100644 --- a/public/models/props/FONTE.md +++ b/public/models/props/FONTE.md @@ -174,3 +174,22 @@ central — escala no call-site do `map_mansao.js` (tabela JARDIM_VEG). - `samambaia.glb` — "Emerald Feather Clump", touceira densa de samambaia. 3.757 tris. Chat: . Registro: `samambaia-jardim`. +- `penitenciaria_guarita.glb` — "Weathered Searchlight Guarita", torre de + vigilância de presídio com escada metálica, cabine octogonal envidraçada, + telhado de zinco e holofote no canto da cabine. 4.749 tris, 1 primitiva, + texturas WebP 1024. Mint text-to-3D, gerado 25/08/2026 para o rebuild USANTOS + da penitenciaria. Chat: . + Registro: `guarita-vigilancia-penitenciaria`. Otimizado por + `tools/optimize-props.mjs` (1.110 KB → 431 KB). Saiu de cena na USANTOS-R2 + (substituída pela `torre_vigilancia.glb`) e segue no acervo. A torre + procedural fica de fallback (colisores idênticos) e o SpotLight que + varre o pátio (NV2) é sempre procedural — o GLB é só visual. +- `torre_vigilancia.glb` — "Prison Guard Tower", torre de vigilância de + presídio com quatro pernas de concreto em treliça X, escada metálica, + varanda com guarda-corpo e cabine envidraçada com telhado. 4.535 tris, + 1 primitiva. Mint text-to-3D, gerado 26/08/2026 para a frente USANTOS-R2 + (kit penitenciaria_gelo). Chat: + . Registro: + `torre-vigilancia-penitenciaria`. Substitui o visual da + `penitenciaria_guarita.glb` nas 4 guaritas de canto do muro; o fallback + procedural e os colisores são os mesmos nos dois mundos. diff --git a/public/models/props/penitenciaria_guarita.glb b/public/models/props/penitenciaria_guarita.glb new file mode 100644 index 00000000..d13f2d3b Binary files /dev/null and b/public/models/props/penitenciaria_guarita.glb differ diff --git a/public/models/props/torre_vigilancia.glb b/public/models/props/torre_vigilancia.glb new file mode 100644 index 00000000..31f75501 Binary files /dev/null and b/public/models/props/torre_vigilancia.glb differ diff --git a/tools/eval/look-check.mjs b/tools/eval/look-check.mjs index 6fbacd64..e609e5f7 100644 --- a/tools/eval/look-check.mjs +++ b/tools/eval/look-check.mjs @@ -45,7 +45,7 @@ const MUT = (process.argv.find((a) => a.startsWith('--mutante=')) || '').split(' const TETO_DE = 8; // Piloto do RC1 (plans/23): os 3 primeiros mapas do look por mapa. O rollout // estende esta lista aos demais mapas com céu webp. -const MAPAS = ['mansao', 'corrego', 'campomorro']; +const MAPAS = ['mansao', 'corrego', 'campomorro', 'penitenciaria']; // rollout: +penitenciaria (frente USANTOS mapa 2, 25/08) /* ---------- sRGB -> Lab (D65), ΔE CIE76 ---------- */ function srgbParaLab(hex) { diff --git a/tools/eval/look-horizonte.json b/tools/eval/look-horizonte.json index 49999f63..d050425d 100644 --- a/tools/eval/look-horizonte.json +++ b/tools/eval/look-horizonte.json @@ -8,6 +8,15 @@ "n": 19596, "zenite": "6595bf" }, + "sky_penitenciaria.webp": { + "banda": [ + 498, + 510 + ], + "horizonte": "567186", + "n": 22620, + "zenite": "5e85a3" + }, "sky_rj.webp": { "banda": [ 429, diff --git a/tools/eval/map_check.json b/tools/eval/map_check.json index f5986f4b..6ae508de 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-25T05:36:32.174Z", "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", diff --git a/tools/eval/penitenciaria-vida-check.mjs b/tools/eval/penitenciaria-vida-check.mjs new file mode 100644 index 00000000..3b857574 --- /dev/null +++ b/tools/eval/penitenciaria-vida-check.mjs @@ -0,0 +1,272 @@ +/* ============================================================================ + penitenciaria-vida-check.mjs — A PENITENCIÁRIA NÃO PODE PARECER LOW POLY + (frente USANTOS, mapa 2). + ---------------------------------------------------------------------------- + POR QUE EXISTE — frase do dono, 25/08/2026, com estas palavras: + "Os mapas do usantos são os mais low poly do jogo. A ideia é os mapas não + parecerem low poly." + Irmã da parque-vida-check (mesma frente, mesmo padrão de mutantes). O que a + penitenciária ATUAL entrega, medido neste commit pelo próprio harness: + 1.167 meshes (432 deles são torus clonados de arame farpado), 5 texturas + de canvas distintas, muros de concreto chapado, guaritas sem holofote, + celas-caixa sem varanda e nenhum céu/LOOK próprio. Esta régua congela o + que a reconstrução entrega. + + O QUE ELA MEDE (no MESMO mundo do jogo: build real via harness, node puro) + NV1 reboco descascado NOS MUROS: os meshes com collider tag 'muro-*' + carregam material.map nomeado 'penitenciaria-reboco*' E a textura é + medida de verdade — tem que ser DataTexture (canvas é stub em node: + lição 3/5, a régua não pode medir outro mundo). Sobre os pixels: + desvio-padrão de luminância ≥ 14 (reboco creme × concreto/tijolo + exposto = dois estados, não gradiente suave), ≥ 20 baldes de cor + distintos (ruído+fiadas) e diferença de luminância entre as faixas + de topo e base ≥ 8 (umidade na base). Limiares: 2/3 dos valores + medidos na textura gerada (ver comentário na cláusula). + NV2 holofotes que VARREM: 4 objetos 'penitenciaria-holofote-*' e + world.update(dt,t) exportado; entre dois updates com t distante a + assinatura de varredura (posição do target do spot/rotação do cone) + muda nos 4. Holofote parado é decoração, não holofote. + NV3 varandas de cela: ≥ 2 grupos 'penitenciaria-varanda-*' (uma por ala) + com guarda-corpo nomeado dentro ('*-guarda-corpo'). + NV4 arame contínuo: meshes 'penitenciaria-arame-*' entre 1 e 6 por lado + cobrindo os 4 lados (hélice única por lado = 4; o estado antigo são + 432 torus soltos sem nome — reprova pelos dois lados: zero nomeados + E nenhuma geometria contínua). + NV5 horizonte próprio: scene.userData.skyUrl gravado pelo applyLook/ + setMapSky (a régua lê o USO — BUG-02) E LOOK.penitenciaria em + public/js/look.js. + NV6 riqueza de superfície: ≥ 12 material.map.name distintos na cena. + Medido no estado atual: 5 (penitenciaria-aco-enferrujado, + -caixa-municao, -concreto, -concreto-escuro, -patio-concreto-gasto). + O rebuild adiciona reboco, galvanizado, arame, grade, poça e pisos. + NV7 molde real nas guaritas (dono, 26/08/2026, com estas palavras: + "todos os mapas do usantos continuam low poly sem moldes 3d bons"): + ≥ 4 wrappers 'penitenciaria-guarita-*' com userData.molde === + 'torre_vigilancia' gravado PELO BUILD (a régua lê o uso registrado, + não a declaração — BUG-02) E 'torre_vigilancia' em + PENITENCIARIA_PROPS E public/models/props/torre_vigilancia.glb em + disco. Mutante: --mutante=sem-moldes. + + FALHA = NÃO SABER MEDIR (lição 5): build que lança, textura de muro sem + pixels legíveis em node ou update ausente reprovam com mensagem de conserto. + + USO + node tools/eval/penitenciaria-vida-check.mjs + node tools/eval/penitenciaria-vida-check.mjs --mutante=sem-reboco # NV1 + node tools/eval/penitenciaria-vida-check.mjs --mutante=holofote-parado # NV2 + node tools/eval/penitenciaria-vida-check.mjs --mutante=sem-varanda # NV3 + node tools/eval/penitenciaria-vida-check.mjs --mutante=sem-moldes # NV7 + ============================================================================ */ +import fs from 'node:fs'; +import { THREE, MAPS, initTextures } from './harness.mjs'; +import { LOOK } from '../../public/js/look.js'; +import { PENITENCIARIA_PROPS } from '../../public/js/map_penitenciaria.js'; + +const MUTANTE = (process.argv.find((a) => a.startsWith('--mutante=')) || '').split('=')[1] || null; +const conhecidos = new Set(['sem-reboco', 'holofote-parado', 'sem-varanda', 'sem-moldes']); +if (MUTANTE && !conhecidos.has(MUTANTE)) throw new Error(`mutante desconhecido: ${MUTANTE}`); + +const MIN_TEXTURAS = 12; + +const T = await initTextures(); +const scene = new THREE.Scene(); +let W = null, erroBuild = null; +try { W = MAPS.penitenciaria.build(scene, T); } catch (e) { erroBuild = String(e?.message || e); } + +const clausulas = []; +const put = (id, ok, msg) => { clausulas.push({ id, ok, msg }); console.log(` ${id} ${ok ? 'PASSA' : 'FALHA'} — ${msg}`); }; + +if (erroBuild || !W?.root) { + put('NV0', false, `não sei medir: build da penitenciária ${erroBuild ? `lançou "${erroBuild}"` : 'devolveu mundo sem root'} — conserte o build antes da régua`); + console.log('\nPENITENCIARIA-VIDA VERMELHA · mundo não medível'); + process.exit(1); +} + +/* ---- mutantes (aplicam DE VERDADE ou morrem — lição 8) ---- */ +let mutanteAplicou = null; +if (MUTANTE === 'sem-reboco') { + let n = 0; + W.root.traverse((o) => { + if (!o.isMesh) return; + for (const m of Array.isArray(o.material) ? o.material : [o.material]) { + if (m?.map?.name?.startsWith('penitenciaria-reboco')) { m.map = null; n++; } + } + }); + mutanteAplicou = n > 0; +} else if (MUTANTE === 'holofote-parado') { + mutanteAplicou = typeof W.update === 'function'; + if (mutanteAplicou) W.update = () => {}; +} else if (MUTANTE === 'sem-varanda') { + const alvos = []; + W.root.traverse((o) => { if (o.name?.startsWith('penitenciaria-varanda-')) alvos.push(o); }); + mutanteAplicou = alvos.length > 0; + for (const o of alvos) o.parent.remove(o); +} else if (MUTANTE === 'sem-moldes') { + let n = 0; + W.root.traverse((o) => { if (o.name?.startsWith('penitenciaria-guarita-') && o.userData?.molde) { delete o.userData.molde; n++; } }); + const i = PENITENCIARIA_PROPS.indexOf('torre_vigilancia'); + if (i >= 0) PENITENCIARIA_PROPS.splice(i, 1); + mutanteAplicou = n > 0; +} +if (MUTANTE && !mutanteAplicou) { + console.error(`MUTANTE NÃO APLICOU: ${MUTANTE} — a régua não mede o que o mutante quebra`); + process.exit(1); +} + +/* ---- NV1 reboco descascado nos muros ---- */ +{ + const muros = []; + W.root.traverse((o) => { if (o.isMesh && o.userData?.collider?.tag?.startsWith('muro-')) muros.push(o); }); + const comReboco = muros.filter((o) => { + const mats = Array.isArray(o.material) ? o.material : [o.material]; + return mats.some((m) => m?.map?.name?.startsWith('penitenciaria-reboco')); + }); + const tex = comReboco.length + ? (Array.isArray(comReboco[0].material) ? comReboco[0].material : [comReboco[0].material]) + .find((m) => m?.map?.name?.startsWith('penitenciaria-reboco'))?.map + : null; + const data = tex?.image?.data; + if (!muros.length) { + put('NV1', false, 'não sei medir: nenhum mesh com collider tag muro-* — o PEN1 cobre os muros?'); + } else if (!comReboco.length) { + put('NV1', false, `${muros.length} muros sem textura 'penitenciaria-reboco*' — concreto chapado é a cara low poly que o dono nomeou; gere o reboco descascado (DataTexture, idioma texProcedural da mansão)`); + } else if (!data || !data.length) { + put('NV1', false, 'não sei medir: a textura de reboco não expõe pixels em node (canvas é stub — lição 3). Use DataTexture pixel-a-pixel, como o texProcedural da mansão'); + } else { + const n = data.length / 4; + let soma = 0, soma2 = 0, cnt = 0, somaTopo = 0, cntTopo = 0, somaBase = 0, cntBase = 0; + const baldes = new Set(); + const altura = tex.image.height || 1; + for (let i = 0; i < n; i += 7) { + const r = data[i * 4], g = data[i * 4 + 1], b = data[i * 4 + 2]; + const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b; + soma += lum; soma2 += lum * lum; cnt++; + baldes.add((r >> 4) << 8 | (g >> 4) << 4 | b >> 4); + const y = Math.floor(i / (tex.image.width || 1)); + if (y < altura * 0.15) { somaBase += lum; cntBase++; } + else if (y > altura * 0.85) { somaTopo += lum; cntTopo++; } + } + const media = soma / cnt; + const desvio = Math.sqrt(Math.max(0, soma2 / cnt - media * media)); + const gradiente = Math.abs(somaTopo / cntTopo - somaBase / cntBase); + /* medido na textura gerada (reboco reforçado, 26/08/2026): desvio 48,6 · + 42 baldes · gradiente 56,8 (reboco creme 204 × concreto 146 × tijolo 118 + × umidade −34 % na base; oitava grossa varia o descascado por trecho). + Limiares bem abaixo do medido: ajuste de paleta passa, + reboco chapado (desvio ≈ 5) e textura sem gradiente não. */ + const falta = []; + if (desvio < 14) falta.push(`desvio de luminância ${desvio.toFixed(1)}/14 (reboco × substrato exposto = dois estados; chapado não descasca)`); + if (baldes.size < 20) falta.push(`${baldes.size}/20 baldes de cor (fiadas + nuance + mancha)`); + if (gradiente < 8) falta.push(`gradiente topo↔base ${gradiente.toFixed(1)}/8 (umidade na base)`); + put('NV1', !falta.length, falta.length + ? falta.join(' · ') + : `${comReboco.length}/${muros.length} muros com reboco medido: desvio ${desvio.toFixed(1)} · ${baldes.size} cores · gradiente ${gradiente.toFixed(1)}`); + } +} + +/* ---- NV2 holofotes que varrem ---- */ +{ + const holofotes = []; + W.root.traverse((o) => { if (o.name?.startsWith('penitenciaria-holofote-')) holofotes.push(o); }); + if (holofotes.length !== 4) { + put('NV2', false, `${holofotes.length}/4 'penitenciaria-holofote-*' — cada guarita ganha um holofote que varre o pátio`); + } else if (typeof W.update !== 'function') { + put('NV2', false, 'não sei medir: o mapa não exporta update(dt, time) — o game.js chama world.update se existir (game.js:7218); exporte-o com a varredura'); + } else { + const assinatura = () => holofotes.map((h) => { + const v = []; + h.traverse((o) => { v.push(o.position.toArray(), o.rotation.toArray().slice(0, 3), o.quaternion.toArray()); }); + return JSON.stringify(v); + }); + const antes = assinatura(); + W.update(0.1, 3.0); W.update(0.1, 3.5); W.update(0.1, 11.0); + const depois = assinatura(); + const parados = holofotes.filter((_, i) => antes[i] === depois[i]).length; + put('NV2', parados === 0, parados + ? `${parados}/4 holofotes PARADOS entre update(t=3) e update(t=11) — holofote fixo é luminária; varra o pátio LENTO no update` + : '4 holofotes varrem entre update(t=3) e update(t=11)'); + } +} + +/* ---- NV3 varandas de cela ---- */ +{ + const varandas = []; + W.root.traverse((o) => { if (/^penitenciaria-varanda-[a-z]+$/.test(o.name || '')) varandas.push(o); }); + const comGuarda = varandas.filter((v) => { + let ok = false; + v.traverse((o) => { if (o.name?.includes('guarda-corpo')) ok = true; }); + return ok; + }); + put('NV3', varandas.length >= 2 && comGuarda.length >= 2, varandas.length >= 2 && comGuarda.length >= 2 + ? `${varandas.length} varandas com guarda-corpo (duas alas)` + : `${varandas.length}/2 varandas, ${comGuarda.length}/2 com guarda-corpo — o 1º pavimento das duas alas ganha passarela metálica 'penitenciaria-varanda-*' com '*-guarda-corpo'`); +} + +/* ---- NV4 arame contínuo ---- */ +{ + const arames = []; + W.root.traverse((o) => { if (o.isMesh && o.name?.startsWith('penitenciaria-arame-')) arames.push(o); }); + const lados = new Set(arames.map((a) => { + const p = new THREE.Vector3(); a.getWorldPosition(p); + const cands = [['sul', Math.abs(p.z + 48)], ['norte', Math.abs(p.z - 48)], ['oeste', Math.abs(p.x + 38)], ['leste', Math.abs(p.x - 38)]]; + cands.sort((x, y) => x[1] - y[1]); + return cands[0][1] < 6 ? cands[0][0] : `solto(${p.x.toFixed(0)},${p.z.toFixed(0)})`; + })); + const falta = []; + if (!arames.length) falta.push('nenhum mesh penitenciaria-arame-* — o arame atual são centenas de torus clonados sem nome: refaça como hélice contínua (TubeGeometry) nomeada por lado'); + else { + if (arames.length > 24) falta.push(`${arames.length} meshes de arame (teto 24 = 6/lado) — hélice contínua, não torus soltos`); + if (lados.size < 4) falta.push(`arame em ${lados.size}/4 lados (${[...lados].join(', ')})`); + } + put('NV4', !falta.length, falta.length ? falta.join(' · ') : `${arames.length} meshes de arame cobrindo 4 lados`); +} + +/* ---- NV5 horizonte próprio ---- */ +{ + const sky = scene.userData.skyUrl; + const look = LOOK.penitenciaria; + const falta = []; + if (!sky) falta.push('scene.userData.skyUrl ausente (o mapa não passou pelo applyLook/setMapSky)'); + if (!look) falta.push('LOOK.penitenciaria ausente em public/js/look.js (APPEND na tabela)'); + put('NV5', !falta.length, falta.length ? falta.join(' · ') : `sky ${sky} · LOOK.penitenciaria ok`); +} + +/* ---- NV6 riqueza de superfície ---- */ +{ + const nomes = new Set(); + W.root.traverse((o) => { + if (!o.isMesh) return; + for (const m of Array.isArray(o.material) ? o.material : [o.material]) if (m?.map?.name) nomes.add(m.map.name); + }); + put('NV6', nomes.size >= MIN_TEXTURAS, nomes.size >= MIN_TEXTURAS + ? `${nomes.size} texturas distintas em uso` + : `${nomes.size}/${MIN_TEXTURAS} texturas distintas (${[...nomes].sort().join(', ') || 'nenhuma'}) — 5 canvas repetidos é a cara low poly; crie superfícies novas (reboco, galvanizado, arame, grade, poça…)`); +} + +/* ---- NV7 molde real nas guaritas ---- */ +{ + const wrappers = []; + W.root.traverse((o) => { if (/^penitenciaria-guarita-\d+$/.test(o.name || '')) wrappers.push(o); }); + const comMolde = wrappers.filter((o) => o.userData?.molde === 'torre_vigilancia'); + const glbPath = new URL('../../public/models/props/torre_vigilancia.glb', import.meta.url); + const falta = []; + if (comMolde.length < 4) falta.push(`${comMolde.length}/4 wrappers 'penitenciaria-guarita-*' com userData.molde='torre_vigilancia' — guarita procedural/caixa é a cara low poly que o dono nomeou; registre o molde no wrapper no build`); + if (!PENITENCIARIA_PROPS.includes('torre_vigilancia')) falta.push(`PENITENCIARIA_PROPS sem 'torre_vigilancia' (${PENITENCIARIA_PROPS.join(', ') || 'vazia'}) — sem o id o main.js não pré-carrega o GLB`); + if (!fs.existsSync(glbPath)) falta.push('public/models/props/torre_vigilancia.glb ausente em disco'); + put('NV7', !falta.length, falta.length ? falta.join(' · ') : '4 guaritas com molde torre_vigilancia registrado + prop pré-carregado + GLB em disco'); +} + +/* ---- placar e veredito dos mutantes ---- */ +const vermelhas = clausulas.filter((c) => !c.ok); +const ALVO = { 'sem-reboco': 'NV1', 'holofote-parado': 'NV2', 'sem-varanda': 'NV3', 'sem-moldes': 'NV7' }; +if (MUTANTE) { + const esperado = ALVO[MUTANTE]; + const acertou = vermelhas.some((c) => c.id === esperado); + const colaterais = vermelhas.filter((c) => c.id !== esperado).map((c) => c.id); + if (!acertou) { console.error(`\nMUTANTE SOBREVIVEU: ${MUTANTE} não acendeu ${esperado}`); process.exit(1); } + if (colaterais.length) { console.error(`\nMUTANTE ${MUTANTE} acendeu cláusulas colaterais: ${colaterais.join(', ')}`); process.exit(1); } + console.log(`\nMUTANTE MORDIDO: ${MUTANTE} -> ${esperado}`); + process.exit(0); +} +console.log(`\nPENITENCIARIA-VIDA ${vermelhas.length ? `VERMELHA · ${vermelhas.map((c) => c.id).join(', ')}` : 'ok · NV1-NV7'}`); +process.exit(vermelhas.length ? 1 : 0);