diff --git a/ARCH.generated.md b/ARCH.generated.md index 78d40edb..15a29da5 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.555 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:parquevida 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..02d414fa 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.555 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 | 158 | `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) | @@ -320,7 +320,7 @@ Os mapas registrados, e em que modo cada um abre: | `upa_24h` | UPA 24h da Treta | **captura** | `map_upa.js` | 302 | | `obras_prefeitura` | Obras da Prefeitura | **captura** | `map_obras.js` | 257 | | `atacadao_treta` | Atacadão da Treta | **captura** | `map_atacadao.js` | 276 | -| `parque_treta` | Parque da Treta | **captura** | `map_parque.js` | 426 | +| `parque_treta` | Parque da Treta | **captura** | `map_parque.js` | 810 | | `velho_oeste` | Velho Oeste da Treta | **captura** | `map_velho_oeste.js` | 449 | | `penitenciaria` | Penitenciária da Treta | **captura** | `map_penitenciaria.js` | 264 | diff --git a/SCRIPTS.md b/SCRIPTS.md index 4204e278..eddea6f6 100644 --- a/SCRIPTS.md +++ b/SCRIPTS.md @@ -122,6 +122,14 @@ A roda-gigante gira em torno do cubo; assentos não invadem lateral, base nem ar npm run eval:parquewheel ``` +## `eval:parquevida` + +O Parque 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"). PV1 mobiliário nomeado (coreto, ≥6 lixeiras, ≥6 bancos, trilho + ≥100 dormentes), PV2 vegetação instanciada (≥40 árvores, ≥60 arbustos), PV3 ≥16 texturas de canvas distintas em uso, PV4 nenhum quadrante 4×4 deserto de cobertura, PV5 horizonte próprio (skyUrl + LOOK.parque_treta). Mutantes: sem-coreto|sem-vegetacao|sem-variedade. + +```bash +npm run eval:parquevida +``` + ## `eval:velhooeste` O mapa Velho Oeste preserva marcos, janelas de madeira abertas/fechadas, gênero, perigo e recompensas dos cartazes, retratos, densidade, colisões, spawns e rota. Mutantes: sem-saloon|sem-carrocas|sem-tumbleweed|sem-obstaculos-centrais|centro-aberto|sem-cartazes|sem-retratos|cartaz-sobre-janela|genero-unico|janela-verde|todas-fechadas|perigoso-unico|recompensa-repetida|sem-colisao-varanda|sem-colisao-movel|parada|texturas-genericas. diff --git a/STATUS.md b/STATUS.md index 8480b985..075f1d16 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.555 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..88001f0a 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.555 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..59a2b7a4 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)` @@ -262,7 +262,7 @@ O registro, gerado do `MAPS` de `public/js/maps.js`: | `upa_24h` | UPA 24h da Treta | **captura** | `map_upa.js` | 302 | | `obras_prefeitura` | Obras da Prefeitura | **captura** | `map_obras.js` | 257 | | `atacadao_treta` | Atacadão da Treta | **captura** | `map_atacadao.js` | 276 | -| `parque_treta` | Parque da Treta | **captura** | `map_parque.js` | 426 | +| `parque_treta` | Parque da Treta | **captura** | `map_parque.js` | 810 | | `velho_oeste` | Velho Oeste da Treta | **captura** | `map_velho_oeste.js` | 449 | | `penitenciaria` | Penitenciária da Treta | **captura** | `map_penitenciaria.js` | 264 | diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index 66039d5b..8af73733 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.555 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 | 158 | `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) | @@ -235,7 +235,7 @@ Os mapas registrados hoje, e em que modo cada um abre: | `upa_24h` | UPA 24h da Treta | **captura** | `map_upa.js` | 302 | | `obras_prefeitura` | Obras da Prefeitura | **captura** | `map_obras.js` | 257 | | `atacadao_treta` | Atacadão da Treta | **captura** | `map_atacadao.js` | 276 | -| `parque_treta` | Parque da Treta | **captura** | `map_parque.js` | 426 | +| `parque_treta` | Parque da Treta | **captura** | `map_parque.js` | 810 | | `velho_oeste` | Velho Oeste da Treta | **captura** | `map_velho_oeste.js` | 449 | | `penitenciaria` | Penitenciária da Treta | **captura** | `map_penitenciaria.js` | 264 | @@ -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:parquevida 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..be468f98 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 **72 assets** gerados via Mint (19 `openrouter-image-concept` · 2 `openrouter-concept-tripo-blender-model` · 5 `imagegen-concept` · 37 `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..45921ff8 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,555 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..c09d718e 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)` @@ -265,7 +265,7 @@ The registry, generated from the `MAPS` of `public/js/maps.js`: | `upa_24h` | UPA 24h da Treta | **capture** | `map_upa.js` | 302 | | `obras_prefeitura` | Obras da Prefeitura | **capture** | `map_obras.js` | 257 | | `atacadao_treta` | Atacadão da Treta | **capture** | `map_atacadao.js` | 276 | -| `parque_treta` | Parque da Treta | **capture** | `map_parque.js` | 426 | +| `parque_treta` | Parque da Treta | **capture** | `map_parque.js` | 810 | | `velho_oeste` | Velho Oeste da Treta | **capture** | `map_velho_oeste.js` | 449 | | `penitenciaria` | Penitenciária da Treta | **capture** | `map_penitenciaria.js` | 264 | 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..da7d04c3 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,555 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 | 158 | `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) | @@ -237,7 +237,7 @@ The maps registered today, and which mode each one opens in: | `upa_24h` | UPA 24h da Treta | **capture** | `map_upa.js` | 302 | | `obras_prefeitura` | Obras da Prefeitura | **capture** | `map_obras.js` | 257 | | `atacadao_treta` | Atacadão da Treta | **capture** | `map_atacadao.js` | 276 | -| `parque_treta` | Parque da Treta | **capture** | `map_parque.js` | 426 | +| `parque_treta` | Parque da Treta | **capture** | `map_parque.js` | 810 | | `velho_oeste` | Velho Oeste da Treta | **capture** | `map_velho_oeste.js` | 449 | | `penitenciaria` | Penitenciária da Treta | **capture** | `map_penitenciaria.js` | 264 | @@ -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:parquevida 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..0c421496 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 **72 assets** generated through Mint (19 `openrouter-image-concept` · 2 `openrouter-concept-tripo-blender-model` · 5 `imagegen-concept` · 37 `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..7858a31e 100644 --- a/mint-assets.json +++ b/mint-assets.json @@ -1883,6 +1883,169 @@ ], "finalSha256": "d5fdc03244bbb0acf4599ea6358b8083a1b6a1f91ce7f9a531c9413ec6052e1c" } + }, + "coreto-praca-parque": { + "artifactType": "model/gltf-binary", + "files": [ + "public/models/props/parque_coreto.glb" + ], + "source": { + "kind": "mint-model", + "assetId": "ks75c1dfd2g0hcxytvzkgeqghs8d4qja", + "chatUrl": "https://mint.gg/chat/ph74qprnsd4nzq113248a9sd6s8d4zhk", + "displayName": "Festive Green Coreto (coreto de praça)", + "frente": "map2-parque", + "notes": "Coreto octogonal com colunas de ferro e bandeirolas (4.742 tris). Prop herói do rebuild USANTOS do parque_treta. GLB normalizado ~1 m, pivô central; escala no call-site (targetH 4,4 / targetLen 7,2)." + }, + "processing": { + "provider": "mint", + "model": "mint text-to-3d (normalized original_glb)", + "tools": [ + "mint.gg", + "tools/optimize-props.mjs" + ], + "finalSha256": "7278c6deb242234459b27c966af1d92afc0fcf425901075d99f237c29e194ae0" + } + }, + "roda-gigante-parque": { + "artifactType": "model/gltf-binary", + "files": [ + "public/models/props/roda_gigante.glb" + ], + "source": { + "kind": "mint-model", + "assetId": "ks74m3x604r82xbtnxt126sqbx8d754g", + "chatUrl": "https://mint.gg/chat/ph71fzmc1gjm6tvbve4b59x4td8d6gvn", + "displayName": "Ferris Wheel (roda-gigante de parque)", + "frente": "map2-parque", + "notes": "Roda-gigante completa: aro duplo, raios, 14 gôndolas, pernas em A e plataforma (3.913 tris). Modelo-fonte do split roda/base; o mapa carrega as duas partes, não este arquivo." + }, + "processing": { + "provider": "mint", + "model": "mint text-to-3d", + "tools": [ + "mint.gg", + "tools/optimize-props.mjs" + ], + "finalSha256": "9f9680d97b7899bedf809e4426c0ba442f164b753fa13465ce43c28cdb7c69c4" + } + }, + "roda-gigante-roda-parque": { + "artifactType": "model/gltf-binary", + "files": [ + "public/models/props/roda_gigante_roda.glb" + ], + "source": { + "kind": "mint-model", + "assetId": "ks74m3x604r82xbtnxt126sqbx8d754g", + "chatUrl": "https://mint.gg/chat/ph71fzmc1gjm6tvbve4b59x4td8d6gvn", + "displayName": "Ferris Wheel — parte rotativa (aro+raios+gôndolas)", + "frente": "map2-parque", + "notes": "Split Blender (2.871 tris) do roda_gigante.glb por componentes conexos: aro duplo, raios e gôndolas. Gira como filha do grupo 'roda-gigante' (rotação procedural do update). Escala no call-site: targetH 24,1, cubo na origem via Box3 (hubFrac 0,537)." + }, + "processing": { + "provider": "mint", + "model": "mint text-to-3d", + "tools": [ + "mint.gg", + "tools/optimize-props.mjs", + "tools/blender-split-roda-gigante.py" + ], + "finalSha256": "b5ba3713bb23cdbb3b617d691532adf79b2e9b31683413d5d5f6df39a2fa205f" + } + }, + "roda-gigante-base-parque": { + "artifactType": "model/gltf-binary", + "files": [ + "public/models/props/roda_gigante_base.glb" + ], + "source": { + "kind": "mint-model", + "assetId": "ks74m3x604r82xbtnxt126sqbx8d754g", + "chatUrl": "https://mint.gg/chat/ph71fzmc1gjm6tvbve4b59x4td8d6gvn", + "displayName": "Ferris Wheel — base estática (pernas A+plataforma)", + "frente": "map2-parque", + "notes": "Split Blender (1.042 tris) do roda_gigante.glb: pernas em A com pés, plataforma cercada e cabine do operador. Estática, pés em y=0; targetH 19,9 casa o cubo da base com o da parte rotativa (escala comum 25,7 m/un)." + }, + "processing": { + "provider": "mint", + "model": "mint text-to-3d", + "tools": [ + "mint.gg", + "tools/optimize-props.mjs", + "tools/blender-split-roda-gigante.py" + ], + "finalSha256": "cbe7321c54a64f87a5935ee4718f5c5e9de3ad595b652092dc3053015a947792" + } + }, + "carrossel-parque": { + "artifactType": "model/gltf-binary", + "files": [ + "public/models/props/carrossel.glb" + ], + "source": { + "kind": "mint-model", + "assetId": "ks7dba32rp93p94y1582xt4cqx8d7z0t", + "chatUrl": "https://mint.gg/chat/ph71fzmc1gjm6tvbve4b59x4td8d6gvn", + "displayName": "Carousel (carrossel de parque)", + "frente": "map2-parque", + "notes": "Carrossel completo com cavalos e cobertura (4.419 tris). Filho do grupo 'carrossel-giratorio' — gira junto com a rotação procedural; targetH 8,5 no call-site." + }, + "processing": { + "provider": "mint", + "model": "mint text-to-3d", + "tools": [ + "mint.gg", + "tools/optimize-props.mjs" + ], + "finalSha256": "0a96f3a605ef36eefe8aa483c13c610391525144bdf96916da5f6f735843c340" + } + }, + "predio-artdeco-entorno-parque": { + "artifactType": "model/gltf-binary", + "files": [ + "public/models/props/predio_artdeco.glb" + ], + "source": { + "kind": "mint-model", + "assetId": "ks7a5tm3psp3hb90m50mq548m18d6bpa", + "chatUrl": "https://mint.gg/chat/ph71fzmc1gjm6tvbve4b59x4td8d6gvn", + "displayName": "Art Deco Building (prédio de entorno)", + "frente": "map2-parque", + "notes": "Prédio art-déco (4.297 tris) instanciado 8× fora dos bounds jogáveis do parque_treta, fechando o quarteirão; rotações e alturas variadas (15–19 m), sem colisor." + }, + "processing": { + "provider": "mint", + "model": "mint text-to-3d", + "tools": [ + "mint.gg", + "tools/optimize-props.mjs" + ], + "finalSha256": "54772ff857273da0630f46d9fe7b0f67dfbc3dad71ed123eb1f085e7ca7717cc" + } + }, + "barraca-quermesse-parque": { + "artifactType": "model/gltf-binary", + "files": [ + "public/models/props/barraca_quermesse.glb" + ], + "source": { + "kind": "mint-model", + "assetId": "ks77wmwqmw4ef5dm24h3mbhfas8d6xh0", + "chatUrl": "https://mint.gg/chat/ph71fzmc1gjm6tvbve4b59x4td8d6gvn", + "displayName": "Quermesse Tent (barraca de quermesse)", + "frente": "map2-parque", + "notes": "Barraca de quermesse (4.419 tris) substituindo os 4 quiosques-caixa do parque_treta (PIPOCA/ALGODÃO DOCE/PESCARIA/ARGOLA); placas signTexture e colisores procedurais mantidos. targetH 3,2." + }, + "processing": { + "provider": "mint", + "model": "mint text-to-3d", + "tools": [ + "mint.gg", + "tools/optimize-props.mjs" + ], + "finalSha256": "bef57d0ea1310119356cb9104c0d916fdcf6f7b82b9981f18136e4405bc3f204" + } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 7d705e10..abd28efb 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "eval:mapcontrato": "node tools/eval/map-contrato-check.mjs", "eval:ambience-registry": "node tools/eval/ambience-registry-check.mjs", "eval:parquewheel": "node tools/eval/parque-wheel-check.mjs", + "eval:parquevida": "node tools/eval/parque-vida-check.mjs", "eval:velhooeste": "node tools/eval/velho-oeste-check.mjs", "eval:penitenciaria": "node tools/eval/penitenciaria-check.mjs", "eval:vminspect": "node tools/eval/vm-inspect-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:parquevida 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", "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_parque.webp b/public/img/textures/sky_parque.webp new file mode 100644 index 00000000..40d81a5a Binary files /dev/null and b/public/img/textures/sky_parque.webp differ diff --git a/public/js/look.js b/public/js/look.js index e5562a14..4fd7f259 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 }, }, + parque_treta: { // fim de tarde no parque: sol baixo a oeste, sombras longas + sky: '/img/textures/sky_parque.webp', + horizonte: 0x7b739a, // look-horizonte.py sobre sky_parque.webp (banda 498-510 de 1024) + zenite: 0x193660, + sol: { cor: 0xffc890, i: 1.75, pos: [-38, 16, -8] }, + hemi: { ceu: 0xd9c2e0, chao: 0x4f4034, i: 1.0 }, + neblina: { d: 0.0075, solDir: [-38, 16, -8], forca: 0.88 }, + grade: { exposicao: 1.42, piso: 0.0050, expAces: 1.52 }, + }, }; diff --git a/public/js/map_parque.js b/public/js/map_parque.js index e010feca..a2fdbe55 100644 --- a/public/js/map_parque.js +++ b/public/js/map_parque.js @@ -1,13 +1,28 @@ -// Parque da Treta: arena CTF simétrica, colorida e inteiramente procedural. +// Parque da Treta: arena CTF simétrica em fim de tarde, inteiramente procedural. +// Rebuild USANTOS (régua: tools/eval/parque-vida-check.mjs). import * as THREE from 'three'; +import { InstBatch, mergeParts, placeProp } from './mapprops.js'; +import { applyLook } from './map_sky.js'; import { createFavelaAmbience } from './ambientlife.js'; import { AMB_LOOPS } from './soundscape.js'; +/* props GLB do mapa (Mint, FONTE.md): os moldes substituem o procedural no + browser; no arnês node placeProp devolve null e o procedural cobre (mesma + estrutura de colisores nos dois mundos — lição 3). */ +export const PARQUE_PROPS = ['parque_coreto', 'roda_gigante_roda', 'roda_gigante_base', 'carrossel', 'barraca_quermesse', 'predio_artdeco']; +export const PARQUE_AMBIENCE = ['rat', 'pigeonGround', 'dog', 'parrot']; + const HALF_X = 32; const HALF_Z = 42; const WHEEL_X = -19; const WHEEL_Y = 14.5; const WHEEL_FRAME_Z = -1.2; +/* Molde Mint da roda (split Blender): escala casa o cubo do molde com WHEEL_Y — + (0,065-(-0,499))·25,7 = 14,5; hubFrac 0,537 e h 0,938/0,775 medidos nos GLBs. */ +const RODA_ESCALA = 25.7, RODA_HUB_FRAC_Y = 0.537, RODA_Z = 0.55; +const RODA_H = 0.938 * RODA_ESCALA, RODA_BASE_H = 0.775 * RODA_ESCALA; +const CORETO = { x: -25, z: -22.5 }; +const ESTACAO = { x: 24.5, z: 16.5 }; export function buildParque(scene, T) { const colliders = []; @@ -46,6 +61,49 @@ export function buildParque(scene, T) { } else if (kind === 'water') { ctx.strokeStyle = accent; ctx.lineWidth = 1.5; for (let y = 6; y < 128; y += 10) { ctx.beginPath(); for (let x = 0; x <= 128; x += 4) ctx.lineTo(x, y + Math.sin(x * 0.18 + y) * 2.5); ctx.stroke(); } + } else if (kind === 'folha') { + for (let i = 0; i < 90; i++) { + const x = rand() * 128, y = rand() * 128, r = 3 + rand() * 7; + ctx.fillStyle = accent; ctx.globalAlpha = 0.25 + rand() * 0.45; + ctx.beginPath(); ctx.ellipse(x, y, r, r * (0.45 + rand() * 0.4), rand() * Math.PI, 0, Math.PI * 2); ctx.fill(); + } + ctx.globalAlpha = 0.5; ctx.strokeStyle = 'rgba(12,42,18,.8)'; ctx.lineWidth = 1; + for (let i = 0; i < 40; i++) { const x = rand() * 128, y = rand() * 128; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + (rand() - 0.5) * 9, y + (rand() - 0.5) * 9); ctx.stroke(); } + ctx.globalAlpha = 1; + } else if (kind === 'casca') { + ctx.strokeStyle = accent; ctx.lineWidth = 2.5; + for (let x = 4; x < 128; x += 9) { ctx.beginPath(); ctx.moveTo(x, 0); for (let y = 0; y <= 128; y += 8) ctx.lineTo(x + Math.sin(y * 0.11 + x) * 2.5, y); ctx.stroke(); } + ctx.fillStyle = 'rgba(28,14,6,.4)'; for (let i = 0; i < 14; i++) ctx.fillRect(rand() * 128, rand() * 128, 2 + rand() * 3, 5 + rand() * 9); + } else if (kind === 'terra') { + ctx.fillStyle = accent; + for (let i = 0; i < 650; i++) { ctx.globalAlpha = 0.15 + rand() * 0.3; ctx.fillRect(rand() * 128, rand() * 128, 0.6 + rand() * 2.4, 0.6 + rand() * 2.4); } + ctx.globalAlpha = 1; ctx.fillStyle = 'rgba(60,42,26,.5)'; + for (let i = 0; i < 26; i++) { ctx.beginPath(); ctx.arc(rand() * 128, rand() * 128, 0.8 + rand() * 1.8, 0, Math.PI * 2); ctx.fill(); } + } else if (kind === 'lona') { + ctx.fillStyle = accent; + for (let x = 0; x < 128; x += 32) ctx.fillRect(x, 0, 16, 128); + ctx.fillStyle = 'rgba(0,0,0,.08)'; for (let x = 15; x < 128; x += 32) ctx.fillRect(x, 0, 2, 128); + ctx.fillStyle = 'rgba(255,255,255,.10)'; for (let i = 0; i < 40; i++) ctx.fillRect(rand() * 128, rand() * 128, 2, 1); + } else if (kind === 'telhado') { + ctx.strokeStyle = accent; ctx.lineWidth = 3; + for (let y = 10; y < 140; y += 18) for (let x = -8; x < 140; x += 20) { ctx.beginPath(); ctx.arc(x + (y % 36 ? 10 : 0), y, 10, Math.PI, 0); ctx.stroke(); } + ctx.fillStyle = 'rgba(40,12,6,.22)'; for (let i = 0; i < 60; i++) ctx.fillRect(rand() * 128, rand() * 128, 2, 2); + } else if (kind === 'lata') { + ctx.strokeStyle = accent; ctx.lineWidth = 3; + for (let x = 6; x < 128; x += 14) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, 128); ctx.stroke(); } + ctx.strokeStyle = 'rgba(255,255,255,.20)'; ctx.lineWidth = 1.5; + for (let x = 11; x < 128; x += 14) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, 128); ctx.stroke(); } + ctx.fillStyle = 'rgba(30,34,38,.35)'; for (let i = 0; i < 40; i++) ctx.fillRect(rand() * 128, rand() * 128, 1.5, 1.5); + } else if (kind === 'pedra') { + ctx.strokeStyle = accent; ctx.lineWidth = 2.5; + for (let y = 0; y <= 128; y += 22) { ctx.beginPath(); ctx.moveTo(0, y); for (let x = 0; x <= 128; x += 16) ctx.lineTo(x, y + (rand() - 0.5) * 5); ctx.stroke(); } + for (let y = 0; y < 128; y += 22) for (let x = (y % 44 ? 12 : 30); x < 128; x += 34) { ctx.beginPath(); ctx.moveTo(x + (rand() - 0.5) * 4, y); ctx.lineTo(x + (rand() - 0.5) * 4, y + 22); ctx.stroke(); } + ctx.fillStyle = 'rgba(255,255,255,.07)'; for (let i = 0; i < 90; i++) ctx.fillRect(rand() * 128, rand() * 128, 2, 2); + } else if (kind === 'asfalto') { + ctx.fillStyle = accent; + for (let i = 0; i < 700; i++) { ctx.globalAlpha = 0.12 + rand() * 0.25; ctx.fillRect(rand() * 128, rand() * 128, 0.5 + rand() * 1.6, 0.5 + rand() * 1.6); } + ctx.globalAlpha = 1; ctx.strokeStyle = 'rgba(20,21,24,.5)'; ctx.lineWidth = 1; + for (let i = 0; i < 6; i++) { let x = rand() * 128, y = rand() * 128; ctx.beginPath(); ctx.moveTo(x, y); for (let k = 0; k < 5; k++) { x += (rand() - 0.5) * 22; y += (rand() - 0.5) * 22; ctx.lineTo(x, y); } ctx.stroke(); } } else { ctx.fillStyle = accent; for (let i = 0; i < 260; i++) { @@ -65,6 +123,10 @@ export function buildParque(scene, T) { tiles: surfaceTexture('tiles', '#dba93f', '#b77e27', 5), paint: surfaceTexture('paint', '#eeeeea', '#5e6772', 3), metal: surfaceTexture('metal', '#aeb6bd', '#2f3942', 3), wood: surfaceTexture('wood', '#9b6039', '#5d321f', 4), hedge: surfaceTexture('hedge', '#287b48', '#174e33', 5), water: surfaceTexture('water', '#43c9f2', 'rgba(230,255,255,.6)', 3), + folha: surfaceTexture('folha', '#2e7f36', '#5cb85a', 2), casca: surfaceTexture('casca', '#6a4a30', '#3d2716', 2), + terra: surfaceTexture('terra', '#8a6a48', '#a98a5e', 6), lona: surfaceTexture('lona', '#e8506a', '#fff2e2', 2), + telhado: surfaceTexture('telhado', '#a8472f', '#6e2a1a', 3), lata: surfaceTexture('lata', '#7f8c94', '#4a545c', 2), + pedra: surfaceTexture('pedra', '#9a938a', '#6e6862', 4), asfalto: surfaceTexture('asfalto', '#4a4d52', '#65686e', 6), }; const lam = (opts = {}) => new THREE.MeshStandardMaterial({ roughness: 0.62, metalness: 0, map: SURFACE.paint, ...opts }); const material = (color, surface = SURFACE.paint, roughness = 0.62, metalness = 0) => new THREE.MeshStandardMaterial({ color, map: surface, roughness, metalness }); @@ -74,6 +136,11 @@ export function buildParque(scene, T) { yellow: material(0xffd84d, SURFACE.paint, 0.48), red: material(0xf04b4b, SURFACE.paint, 0.44), purple: material(0x7b55d9, SURFACE.paint, 0.5), green: material(0x3ec67d, SURFACE.paint, 0.55), white: material(0xfff7e8, SURFACE.paint, 0.58), dark: material(0x39445a, SURFACE.metal, 0.34, 0.62), wood: material(0xffffff, SURFACE.wood, 0.88), hedge: material(0xffffff, SURFACE.hedge, 1), + trunk: material(0xffffff, SURFACE.casca, 0.95), leaf: material(0xffffff, SURFACE.folha, 1), + dirt: material(0xffffff, SURFACE.terra, 1), + roof: material(0xffffff, SURFACE.telhado, 0.82), tin: material(0xffffff, SURFACE.lata, 0.48, 0.35), + stone: material(0xffffff, SURFACE.pedra, 0.9), asphalt: material(0xffffff, SURFACE.asfalto, 0.95), + rail: material(0x9aa4ac, SURFACE.metal, 0.3, 0.75), water: new THREE.MeshStandardMaterial({ color: 0xffffff, map: SURFACE.water, roughness: 0.18, metalness: 0.05, transparent: true, opacity: 0.76 }), cloud: new THREE.MeshStandardMaterial({ color: 0xfffdf5, roughness: 1 }), }; @@ -127,9 +194,9 @@ export function buildParque(scene, T) { return mesh; } - function addTube(points, radius, mat, tubular = 80) { - const curve = new THREE.CatmullRomCurve3(points, false, 'catmullrom', 0.2); - const mesh = new THREE.Mesh(new THREE.TubeGeometry(curve, tubular, radius, 7, false), mat); + function addTube(points, radius, mat, tubular = 80, closed = false) { + const curve = new THREE.CatmullRomCurve3(points, closed, 'catmullrom', 0.2); + const mesh = new THREE.Mesh(new THREE.TubeGeometry(curve, tubular, radius, 7, closed), mat); mesh.castShadow = true; root.add(mesh); return mesh; @@ -181,8 +248,14 @@ export function buildParque(scene, T) { return texture; } - scene.background = new THREE.Color(0x75cef2); - scene.fog = new THREE.Fog(0x92d9ef, 76, 155); + /* Look de fim de tarde (LOOK.parque_treta): 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, 'parque_treta'); + sun.shadow.mapSize.set(2048, 2048); + sun.shadow.camera.left = -46; sun.shadow.camera.right = 46; + sun.shadow.camera.top = 54; sun.shadow.camera.bottom = -54; + sun.shadow.camera.far = 220; sun.shadow.bias = -0.0005; + addCloud(-34, 22, -24, 2.2, 0.55); addCloud(8, 27, -34, 1.8, 0.38); addCloud(30, 20, -18, 1.65, 0.68); @@ -190,11 +263,27 @@ export function buildParque(scene, T) { addBird(-28, 18, -20, 2.4, 2.0, 0.0); addBird(-20, 20, -25, 2.0, 2.3, 1.4); addBird(18, 22, -28, 1.8, 1.7, 2.8); + + // Pisos: grama, caminhos e praça ganham meio-fio, borda e manchas de terra batida. addFloor(HALF_X * 2, HALF_Z * 2, MAT.grass, 0, 0); addFloor(12, HALF_Z * 2, MAT.path, 0, 0, 0.025); addFloor(HALF_X * 2, 10, MAT.path, 0, 0, 0.03); addFloor(24, 24, MAT.plaza, 0, 0, 0.04); for (const sx of [-1, 1]) addFloor(8, HALF_Z * 2 - 6, MAT.path, sx * 22, 0, 0.025); + for (const sx of [-1, 1]) { + addBox(0.28, 0.13, HALF_Z * 2 - 6, MAT.stone, sx * 6.15, 0, 0, { collide: false }); + addBox(HALF_X * 2 - 6, 0.13, 0.28, MAT.stone, 0, 0, sx * 5.15, { collide: false }); + addBox(0.28, 0.12, HALF_Z * 2 - 8, MAT.stone, sx * 26.15, 0, 0, { collide: false }); + } + for (const [x, z, w, d, ry] of [[0, -33, 9, 5, 0], [0, 33, 9, 5, 0], [-14, -16.5, 6, 4, 0.3], [14, 16.5, 6, 4, -0.3], [CORETO.x + 4.5, CORETO.z + 4.5, 5, 4, 0.5]]) { + const patch = new THREE.Mesh(new THREE.PlaneGeometry(w, d), MAT.dirt); + patch.rotation.x = -Math.PI / 2; patch.rotation.z = ry; patch.position.set(x, 0.045, z); + patch.receiveShadow = true; root.add(patch); + } + for (const s of [-1, 1]) { + addBox(0.5, 0.1, 24.4, MAT.stone, s * 12.2, 0, 0, { collide: false }); + addBox(24.4, 0.1, 0.5, MAT.stone, 0, 0, s * 12.2, { collide: false }); + } // Cerca viva perimetral: contém a arena sem esconder o céu e os brinquedos. addBox(HALF_X * 2, 2.2, 0.8, MAT.hedge, 0, 0, -HALF_Z + 0.4); @@ -202,6 +291,18 @@ export function buildParque(scene, T) { addBox(0.8, 2.2, HALF_Z * 2, MAT.hedge, -HALF_X + 0.4, 0, 0); addBox(0.8, 2.2, HALF_Z * 2, MAT.hedge, HALF_X - 0.4, 0, 0); + /* Entorno art-déco (Mint): fecha o quarteirão fora dos bounds jogáveis — + sem colisor, o hedge já contém o jogador. 8 instâncias (PV6). */ + const PREDIOS = [ + [-36.5, -18, 0.35, 17], [-36.5, 16, -0.25, 19], [36.5, -14, 2.9, 16], [36.5, 20, 3.4, 18], + [-14, -46.5, 1.65, 17], [16, -46.5, 1.35, 15], [-16, 46.5, 4.75, 18], [14, 46.5, 4.55, 16], + ]; + PREDIOS.forEach(([px, pz, ry, h], i) => { + const wrap = new THREE.Group(); wrap.name = `parque-molde-predio-${i}`; root.add(wrap); + const glb = placeProp('predio_artdeco', { x: px, y: 0, z: pz, targetH: h, ry }); + if (glb) wrap.add(glb); + }); + // Portal de entrada em cada base. for (const sz of [-1, 1]) { for (const sx of [-1, 1]) addBox(1.1, 7, 1.1, sx < 0 ? MAT.pink : MAT.blue, sx * 7, 0, sz * 35); @@ -211,17 +312,21 @@ export function buildParque(scene, T) { } // Carrossel central: marco de orientação e cobertura circular baixa. - addCylinder(6.1, 0.55, MAT.purple, 0, 0, 0); - addCylinder(0.55, 7.8, MAT.yellow, 0, 0.55, 0); + const carrosselBase = addCylinder(6.1, 0.55, MAT.purple, 0, 0, 0); + const carrosselMastro = addCylinder(0.55, 7.8, MAT.yellow, 0, 0.55, 0); const carousel = new THREE.Group(); carousel.name = 'carrossel-giratorio'; root.add(carousel); animated.carousel = carousel; + const moldeCarrossel = new THREE.Group(); moldeCarrossel.name = 'parque-molde-carrossel'; carousel.add(moldeCarrossel); + const glbCarrossel = placeProp('carrossel', { targetH: 8.5 }); + if (glbCarrossel) moldeCarrossel.add(glbCarrossel); + carrosselBase.visible = carrosselMastro.visible = !glbCarrossel; const canopy = new THREE.Mesh(new THREE.ConeGeometry(7, 3.2, 16), MAT.pink); - canopy.position.set(0, 7.2, 0); canopy.castShadow = true; carousel.add(canopy); + canopy.position.set(0, 7.2, 0); canopy.castShadow = true; canopy.visible = !glbCarrossel; carousel.add(canopy); const canopyTop = new THREE.Mesh(new THREE.ConeGeometry(3.5, 1.7, 16), MAT.yellow); - canopyTop.position.set(0, 8.7, 0); carousel.add(canopyTop); + canopyTop.position.set(0, 8.7, 0); canopyTop.visible = !glbCarrossel; carousel.add(canopyTop); for (let i = 0; i < 8; i++) { const a = i * Math.PI / 4, x = Math.cos(a) * 4.25, z = Math.sin(a) * 4.25; - const pole = new THREE.Mesh(new THREE.CylinderGeometry(0.09, 0.09, 5.2, 8), MAT.white); pole.position.set(x, 3.15, z); carousel.add(pole); - const horse = new THREE.Group(); horse.name = `carrossel-cavalo-${i}`; horse.position.set(x, 2.15, z); horse.rotation.y = -a; carousel.add(horse); + const pole = new THREE.Mesh(new THREE.CylinderGeometry(0.09, 0.09, 5.2, 8), MAT.white); pole.position.set(x, 3.15, z); pole.visible = !glbCarrossel; carousel.add(pole); + const horse = new THREE.Group(); horse.name = `carrossel-cavalo-${i}`; horse.position.set(x, 2.15, z); horse.rotation.y = -a; horse.visible = !glbCarrossel; carousel.add(horse); const horseMat = COLORS[i % COLORS.length]; const body = new THREE.Mesh(new THREE.BoxGeometry(1.45, 0.75, 0.48), horseMat); horse.add(body); const neck = new THREE.Mesh(new THREE.BoxGeometry(0.38, 0.85, 0.38), horseMat); neck.position.set(0.58, 0.5, 0); neck.rotation.z = -0.35; horse.add(neck); @@ -231,11 +336,11 @@ export function buildParque(scene, T) { const leg = new THREE.Mesh(new THREE.BoxGeometry(0.13, 0.72, 0.13), MAT.white); leg.position.set(lx, -0.62, lz); horse.add(leg); } animated.horses.push({ horse, phase: i * Math.PI / 2, baseY: 2.15 }); - const bulb = new THREE.Mesh(new THREE.SphereGeometry(0.14, 8, 6), COLORS[(i + 2) % COLORS.length]); bulb.position.set(Math.cos(a) * 6.15, 6.15, Math.sin(a) * 6.15); carousel.add(bulb); + const bulb = new THREE.Mesh(new THREE.SphereGeometry(0.14, 8, 6), COLORS[(i + 2) % COLORS.length]); bulb.position.set(Math.cos(a) * 6.15, 6.15, Math.sin(a) * 6.15); bulb.visible = !glbCarrossel; carousel.add(bulb); } // Detalhes leves: luminárias, floreiras e bandeirolas sem alterar as rotas do FPS. - for (const [x, z] of [[-7, -12], [7, -12], [-7, 12], [7, 12], [-27, -18], [27, -18], [-27, 18], [27, 18]]) { + for (const [x, z] of [[-7, -12], [7, -12], [-7, 12], [7, 12], [-23.5, -20], [23.5, -20], [-23.5, 20], [23.5, 20]]) { addCylinder(0.11, 4.2, MAT.dark, x, 0, z, { collide: false, segments: 7 }); const lamp = new THREE.Mesh(new THREE.SphereGeometry(0.38, 10, 7), MAT.yellow); lamp.position.set(x, 4.25, z); root.add(lamp); } @@ -243,7 +348,7 @@ export function buildParque(scene, T) { addCylinder(1.25, 0.45, MAT.wood, x, 0, z, { collide: false, segments: 12 }); for (let i = 0; i < 5; i++) { const a = i * Math.PI * 0.4; - const flower = new THREE.Mesh(new THREE.SphereGeometry(0.2, 7, 5), COLORS[(i + (x > 0 ? 2 : 0)) % COLORS.length]); + const flower = new THREE.Mesh(new THREE.SphereGeometry(0.2, 7, 5), lam({ color: COLORS[(i + (x > 0 ? 2 : 0)) % COLORS.length].color, map: SURFACE.folha, roughness: 0.85 })); flower.position.set(x + Math.cos(a) * 0.68, 0.62, z + Math.sin(a) * 0.68); root.add(flower); } } @@ -255,23 +360,38 @@ export function buildParque(scene, T) { // Roda-gigante no flanco oeste; estrutura visual fica fora do corredor jogável. { const wheel = new THREE.Group(); wheel.name = 'roda-gigante'; wheel.position.set(WHEEL_X, WHEEL_Y, 0); root.add(wheel); animated.wheel = wheel; + /* Molde Mint em 2 GLBs: a roda gira dentro do grupo (a rotação é a do update, + rotation.z — nada de órbita nova), a base fica estática no chão. */ + const moldeRoda = new THREE.Group(); moldeRoda.name = 'parque-molde-roda'; wheel.add(moldeRoda); + const glbRoda = placeProp('roda_gigante_roda', { targetH: RODA_H }); + if (glbRoda) { + const b = new THREE.Box3().setFromObject(glbRoda); + glbRoda.position.x -= (b.min.x + b.max.x) / 2; + glbRoda.position.y -= b.min.y + (b.max.y - b.min.y) * RODA_HUB_FRAC_Y; + glbRoda.position.z += WHEEL_FRAME_Z - (b.min.z + b.max.z) / 2; + glbRoda.scale.z *= RODA_Z; + moldeRoda.add(glbRoda); + } + const moldeBase = new THREE.Group(); moldeBase.name = 'parque-molde-roda-base'; root.add(moldeBase); + const glbBase = placeProp('roda_gigante_base', { x: WHEEL_X, y: 0, z: 0, targetH: RODA_BASE_H }); + if (glbBase) { glbBase.scale.z *= RODA_Z; moldeBase.add(glbBase); } const rimMat = MAT.white, hubMat = MAT.yellow; - const rim = new THREE.Mesh(new THREE.TorusGeometry(10, 0.32, 8, 48), rimMat); rim.name = 'roda-aro'; rim.position.z = WHEEL_FRAME_Z; wheel.add(rim); - const hub = new THREE.Mesh(new THREE.CylinderGeometry(0.8, 0.8, 1.4, 12), hubMat); hub.name = 'roda-cubo'; hub.rotation.x = Math.PI / 2; wheel.add(hub); + const rim = new THREE.Mesh(new THREE.TorusGeometry(10, 0.32, 8, 48), rimMat); rim.name = 'roda-aro'; rim.position.z = WHEEL_FRAME_Z; rim.visible = !glbRoda; wheel.add(rim); + const hub = new THREE.Mesh(new THREE.CylinderGeometry(0.8, 0.8, 1.4, 12), hubMat); hub.name = 'roda-cubo'; hub.rotation.x = Math.PI / 2; hub.visible = !glbRoda; wheel.add(hub); for (let i = 0; i < 10; i++) { const a = i * Math.PI / 5, x = Math.cos(a) * 10, y = 12 + Math.sin(a) * 10; const spoke = new THREE.Mesh(new THREE.CylinderGeometry(0.09, 0.09, 10, 6), MAT.white); - spoke.position.set(x / 2, (y - 12) / 2, WHEEL_FRAME_Z); spoke.rotation.z = a - Math.PI / 2; wheel.add(spoke); - const hanger = new THREE.Group(); hanger.name = `roda-cadeira-${i}`; hanger.position.set(x, y - 12, 0); wheel.add(hanger); + spoke.position.set(x / 2, (y - 12) / 2, WHEEL_FRAME_Z); spoke.rotation.z = a - Math.PI / 2; spoke.visible = !glbRoda; wheel.add(spoke); + const hanger = new THREE.Group(); hanger.name = `roda-cadeira-${i}`; hanger.position.set(x, y - 12, 0); hanger.visible = !glbRoda; wheel.add(hanger); const arm = new THREE.Mesh(new THREE.CylinderGeometry(0.07, 0.07, 1.8, 6), MAT.dark); arm.position.y = -0.9; hanger.add(arm); const cabin = new THREE.Mesh(new THREE.BoxGeometry(2.0, 1.4, 1.5), COLORS[i % COLORS.length]); cabin.name = `roda-cabine-${i}`; cabin.position.y = -2.1; hanger.add(cabin); animated.cabins.push({ hanger, phase: a }); } - for (const sx of [-1, 1]) addTube([new THREE.Vector3(WHEEL_X, 0.2, sx * 2.2), new THREE.Vector3(WHEEL_X, WHEEL_Y, 0)], 0.28, MAT.dark, 10); + for (const sx of [-1, 1]) addTube([new THREE.Vector3(WHEEL_X, 0.2, sx * 2.2), new THREE.Vector3(WHEEL_X, WHEEL_Y, 0)], 0.28, MAT.dark, 10).visible = !glbBase; const wheelBase = addBox(6.5, 1.5, 3.8, MAT.blue, WHEEL_X, 0, 0); wheelBase.name = 'roda-base'; // cobertura jogável sob a atração } - // Castelo inflável no flanco leste: silhueta grande e cover fragmentado. + // Castelo inflável no flanco leste: silhueta grande, cover fragmentado, costuras e bocais. { const cx = 24, cz = 0; addBox(8.8, 4.4, 7.4, MAT.purple, cx, 0, cz); @@ -280,6 +400,11 @@ export function buildParque(scene, T) { const roof = new THREE.Mesh(new THREE.ConeGeometry(2.0, 2.6, 10), MAT.yellow); roof.position.set(cx + dx, 7.5, cz + dz); root.add(roof); } addBox(2.4, 3.2, 0.4, MAT.dark, cx, 0, cz - 3.72, { collide: false }); + for (const dx of [-2.2, 0, 2.2]) addBox(0.09, 4.35, 0.06, MAT.purple, cx + dx, 0.02, cz - 3.72, { collide: false, cast: false }); + for (const dx of [-1.6, 1.6]) { + const bocal = new THREE.Mesh(new THREE.CylinderGeometry(0.42, 0.5, 0.9, 10), MAT.blue); + bocal.rotation.x = Math.PI / 2; bocal.position.set(cx + dx, 0.5, cz - 4.15); bocal.castShadow = true; root.add(bocal); + } } // Montanha-russa envolve o fundo sem fechar rotas nem criar colisão complexa. @@ -289,22 +414,29 @@ export function buildParque(scene, T) { ]; addTube(coasterPoints, 0.23, MAT.red); addTube(coasterPoints.map(p => new THREE.Vector3(p.x, p.y, p.z + 1.25)), 0.23, MAT.yellow); - for (const p of coasterPoints) addCylinder(0.14, p.y, MAT.dark, p.x, 0, p.z, { collide: false, segments: 7 }); + for (const p of coasterPoints) addCylinder(0.14, p.y, MAT.trunk, p.x, 0, p.z, { collide: false, segments: 7 }); - // Quiosques espelhados dão cobertura de cintura e quebram linhas de tiro. - function kiosk(x, z, mat, title) { - addBox(5.2, 2.6, 3.8, mat, x, 0, z); - addBox(6.0, 0.4, 4.6, MAT.white, x, 2.6, z, { collide: false }); + // Quiosques espelhados dão cobertura de cintura e quebram linhas de tiro; toldo e balcão tiram a cara de caixa. + function kiosk(x, z, mat, title, idx) { + const wrap = new THREE.Group(); wrap.name = `parque-molde-barraca-${idx}`; root.add(wrap); + const glb = placeProp('barraca_quermesse', { x, y: 0, z, targetH: 3.2, ry: z < 0 ? 0 : Math.PI }); + if (glb) wrap.add(glb); + const caixa = addBox(5.2, 2.6, 3.8, mat, x, 0, z); caixa.visible = !glb; + const toldo = addBox(6.0, 0.4, 4.6, MAT.white, x, 2.6, z, { collide: false }); toldo.visible = !glb; + const front = z < 0 ? 1 : -1; + const balcao = addBox(4.6, 0.9, 0.5, MAT.wood, x, 0, z + front * 2.05); balcao.visible = !glb; + const awning = new THREE.Mesh(new THREE.PlaneGeometry(5.6, 1.7), new THREE.MeshStandardMaterial({ map: SURFACE.lona, roughness: 0.7, side: THREE.DoubleSide })); + awning.position.set(x, 2.85, z + front * 2.35); awning.rotation.x = front * 1.12; awning.castShadow = true; awning.visible = !glb; root.add(awning); + for (const sx of [-1, 1]) addCylinder(0.05, 2.5, MAT.dark, x + sx * 2.6, 0, z + front * 2.9, { collide: false, segments: 6 }).visible = !glb; const sign = new THREE.Mesh(new THREE.PlaneGeometry(4.4, 1.0), new THREE.MeshLambertMaterial({ map: signTexture(title, 'É AQUI!', '#ff4f9a', '#fff7e8') })); sign.position.set(x, 2.15, z + (z < 0 ? 1.93 : -1.93)); sign.rotation.y = z < 0 ? 0 : Math.PI; root.add(sign); } - kiosk(-13, -19, MAT.blue, 'PIPOCA'); kiosk(13, 19, MAT.green, 'ALGODÃO DOCE'); - kiosk(13, -19, MAT.pink, 'PESCARIA'); kiosk(-13, 19, MAT.yellow, 'ARGOLA'); + kiosk(-13, -19, MAT.blue, 'PIPOCA', 0); kiosk(13, 19, MAT.green, 'ALGODÃO DOCE', 1); + kiosk(13, -19, MAT.pink, 'PESCARIA', 2); kiosk(-13, 19, MAT.yellow, 'ARGOLA', 3); - // Barreiras de fila e bancos formam três rotas legíveis, sem labirinto. + // Barreiras de fila formam três rotas legíveis, sem labirinto. for (const sz of [-1, 1]) { for (const x of [-8, 8]) for (const z of [13, 17, 25, 29]) addBox(3.2, 1.15, 0.55, MAT.white, x, 0, sz * z); - for (const x of [-22, 22]) for (const z of [12, 24]) addBox(3.8, 1.0, 1.0, MAT.wood, x, 0, sz * z); } for (const [x, z, mat] of [[-18, -10, MAT.pink], [18, 10, MAT.blue], [18, -10, MAT.yellow], [-18, 10, MAT.green]]) { addCylinder(0.18, 4.4, MAT.dark, x, 0, z); @@ -313,9 +445,266 @@ export function buildParque(scene, T) { // Espelhos d'água rasos decoram as bases sem alterar navegação. for (const sz of [-1, 1]) { - const pond = new THREE.Mesh(new THREE.CircleGeometry(4.2, 24), MAT.water); pond.rotation.x = -Math.PI / 2; pond.position.set(-21, 0.045, sz * 31); root.add(pond); - for (let i = 0; i < 4; i++) addCylinder(0.32, 0.7 + i * 0.18, MAT.white, -23 + i * 1.35, 0, sz * 31, { collide: false, segments: 8 }); + const pond = new THREE.Mesh(new THREE.CircleGeometry(3.2, 24), MAT.water); pond.rotation.x = -Math.PI / 2; pond.position.set(-19, 0.045, sz * 29); root.add(pond); + for (let i = 0; i < 4; i++) addCylinder(0.32, 0.7 + i * 0.18, MAT.white, -20.8 + i * 1.25, 0, sz * 29, { collide: false, segments: 8 }); + } + + // Coreto no quadrante sudoeste: palco octogonal, 8 colunas, telhado e bandeirolas. + { + const g = new THREE.Group(); g.name = 'parque-coreto'; g.position.set(CORETO.x, 0, CORETO.z); root.add(g); + colliders.push({ minX: CORETO.x - 2.9, maxX: CORETO.x + 2.9, minY: 0, maxY: 0.45, minZ: CORETO.z - 2.9, maxZ: CORETO.z + 2.9 }); + const glb = placeProp('parque_coreto', { x: 0, y: 0, z: 0, targetH: 4.4, targetLen: 7.2 }); + if (glb) g.add(glb); + const stage = new THREE.Mesh(new THREE.CylinderGeometry(3.4, 3.65, 0.45, 8), MAT.stone); + stage.position.y = 0.225; stage.castShadow = stage.receiveShadow = true; stage.visible = !glb; g.add(stage); + if (!glb) occluders.push(stage); + const rimTiles = new THREE.Mesh(new THREE.CylinderGeometry(3.42, 3.42, 0.08, 8), MAT.plaza); + rimTiles.position.y = 0.49; rimTiles.receiveShadow = true; rimTiles.visible = !glb; g.add(rimTiles); + for (let i = 0; i < 8; i++) { + const a = i * Math.PI / 4 + Math.PI / 8, cx = Math.cos(a) * 2.9, cz = Math.sin(a) * 2.9; + const col = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.14, 2.4, 8), MAT.white); + col.position.set(cx, 1.65, cz); col.castShadow = true; col.visible = !glb; g.add(col); + colliders.push({ minX: CORETO.x + cx - 0.16, maxX: CORETO.x + cx + 0.16, minY: 0.45, maxY: 2.85, minZ: CORETO.z + cz - 0.16, maxZ: CORETO.z + cz + 0.16 }); + if (!glb) occluders.push(col); + } + const roof = new THREE.Mesh(new THREE.CylinderGeometry(0.7, 4.5, 1.7, 8), MAT.roof); + roof.position.y = 3.75; roof.castShadow = true; roof.visible = !glb; g.add(roof); + const finial = new THREE.Mesh(new THREE.SphereGeometry(0.3, 8, 6), MAT.yellow); finial.position.y = 4.75; finial.visible = !glb; g.add(finial); + for (let i = 0; i < 8; i++) { + const a0 = i * Math.PI / 4 + Math.PI / 8, a1 = (i + 1) * Math.PI / 4 + Math.PI / 8; + for (let k = 1; k <= 3; k++) { + const a = a0 + (a1 - a0) * k / 4; + const flag = new THREE.Mesh(new THREE.ConeGeometry(0.22, 0.5, 3), COLORS[(i + k) % COLORS.length]); + flag.rotation.z = Math.PI; flag.position.set(Math.cos(a) * 2.9, 2.72 - Math.sin(Math.PI * k / 4) * 0.22, Math.sin(a) * 2.9); + flag.visible = !glb; g.add(flag); + } + } + } + + // Trenzinho: circuito contornando o parque (barriga a leste desvia do castelo), + // dois trilhos paralelos + dormentes instanciados + estação com plataforma e placa. + const ringPts = []; + { + const R = 6, X = 27, Z = 34; + for (let z = Z - R; z >= -(Z - R); z -= 2.4) ringPts.push(new THREE.Vector3(X + 2.8 * Math.max(0, Math.cos(Math.PI * z / 14)), 0, z)); + const arc = (cx, cz, a0, a1) => { const n = 8; for (let i = 0; i <= n; i++) { const a = a0 + (a1 - a0) * i / n; ringPts.push(new THREE.Vector3(cx + R * Math.cos(a), 0, cz + R * Math.sin(a))); } }; + arc(X - R, -(Z - R), 0, -Math.PI / 2); + for (let x = X - R; x >= -(X - R); x -= 2.4) ringPts.push(new THREE.Vector3(x, 0, -Z)); + arc(-(X - R), -(Z - R), -Math.PI / 2, -Math.PI); + for (let z = -(Z - R); z <= Z - R; z += 2.4) ringPts.push(new THREE.Vector3(-X, 0, z)); + arc(-(X - R), Z - R, Math.PI, Math.PI / 2); + for (let x = -(X - R); x <= X - R; x += 2.4) ringPts.push(new THREE.Vector3(x, 0, Z)); + arc(X - R, Z - R, Math.PI / 2, 0); + } + const ringCurve = new THREE.CatmullRomCurve3(ringPts, true, 'catmullrom', 0.15); + const ringSamples = ringCurve.getSpacedPoints(300); + for (const off of [-0.35, 0.35]) { + const pts = ringSamples.map((p, i) => { + const t = ringCurve.getTangent(i / ringSamples.length); + const n = new THREE.Vector3(t.z, 0, -t.x).normalize(); + return new THREE.Vector3(p.x + n.x * off, 0.16, p.z + n.z * off); + }); + const rail = addTube(pts, 0.055, MAT.rail, 300, true); + rail.name = `parque-trilho-${off < 0 ? 0 : 1}`; + } + const dormBatch = new InstBatch({ name: 'parque-dormentes' }); + { + const dummy = new THREE.Object3D(); + const step = Math.max(1, Math.floor(ringSamples.length / (ringCurve.getLength() / 0.9))); + for (let i = 0; i < ringSamples.length; i += step) { + const p = ringSamples[i], t = ringCurve.getTangent(i / ringSamples.length); + dummy.position.set(p.x, 0.045, p.z); + dummy.rotation.set(0, Math.atan2(t.x, t.z), 0); + dummy.scale.setScalar(1); dummy.updateMatrix(); + dormBatch.add(boxGeometry(1.05, 0.07, 0.26), MAT.wood, dummy.matrix); + } } + dormBatch.build(root); + { + const g = new THREE.Group(); g.name = 'parque-estacao'; g.position.set(ESTACAO.x, 0, ESTACAO.z); root.add(g); + const platform = new THREE.Mesh(boxGeometry(3.2, 0.4, 2.2), MAT.asphalt); + platform.position.y = 0.2; platform.castShadow = platform.receiveShadow = true; g.add(platform); + colliders.push({ minX: ESTACAO.x - 1.6, maxX: ESTACAO.x + 1.6, minY: 0, maxY: 0.4, minZ: ESTACAO.z - 1.1, maxZ: ESTACAO.z + 1.1 }); + occluders.push(platform); + for (const dz of [-0.85, 0.85]) { + const post = new THREE.Mesh(new THREE.CylinderGeometry(0.07, 0.07, 2.3, 7), MAT.dark); + post.position.set(-0.9, 1.55, dz); post.castShadow = true; g.add(post); + } + const roofE = new THREE.Mesh(boxGeometry(2.6, 0.12, 2.6), MAT.roof); + roofE.position.set(-0.9, 2.75, 0); roofE.rotation.z = 0.1; roofE.castShadow = true; g.add(roofE); + const board = new THREE.Mesh(new THREE.PlaneGeometry(2.8, 0.9), new THREE.MeshLambertMaterial({ map: signTexture('ESTAÇÃO', 'TRENZINHO DO PARQUE', '#2a6e3f', '#fff7a8') })); + board.position.set(-1.05, 2.1, 0); board.rotation.y = -Math.PI / 2; g.add(board); + } + + // Lixeiras instanciadas com variação de cor; bancos de praça, três deles quebrados. + const binGeo = mergeParts([ + new THREE.CylinderGeometry(0.3, 0.26, 0.82, 10).translate(0, 0.41, 0), + new THREE.CylinderGeometry(0.33, 0.33, 0.09, 10).translate(0, 0.87, 0), + ]); + const binBatch = new InstBatch({ name: 'parque-lixeira-lote' }); + const BIN_CORES = [0x2e7d32, 0xf9a825, 0x1565c0]; + { + const dummy = new THREE.Object3D(); + [[-9.5, -13.5], [9.5, 13.5], [-13.5, 17], [13.5, -17], [-24, -9], [24, 9], [-8, 27.5], [8, -27.5]].forEach(([x, z], i) => { + dummy.position.set(x, 0, z); dummy.rotation.set(0, i * 1.3, 0); dummy.scale.setScalar(1); dummy.updateMatrix(); + binBatch.add(binGeo, MAT.tin, dummy.matrix, BIN_CORES[i % BIN_CORES.length]); + colliders.push({ minX: x - 0.32, maxX: x + 0.32, minY: 0, maxY: 0.92, minZ: z - 0.32, maxZ: z + 0.32 }); + }); + } + occluders.push(...binBatch.build(root)); + + const BANCOS = [ + { x: -22, z: 12, ry: Math.PI / 2 }, { x: 22, z: -12, ry: -Math.PI / 2 }, + { x: -22, z: -24, ry: Math.PI / 2, quebrado: 'torta' }, { x: 22, z: 24, ry: -Math.PI / 2 }, + { x: -7.4, z: -21, ry: Math.PI / 2 }, { x: 7.4, z: 21, ry: -Math.PI / 2, quebrado: 'sem-tabua' }, + { x: -25.5, z: 6, ry: 0, quebrado: 'sem-encosto' }, + ]; + BANCOS.forEach((cfg, bi) => { + const g = new THREE.Group(); g.name = `parque-banco-${bi}`; + g.position.set(cfg.x, 0, cfg.z); g.rotation.y = cfg.ry; root.add(g); + for (const lx of [-0.75, 0.75]) { + const leg = new THREE.Mesh(boxGeometry(0.12, 0.44, 0.55), MAT.dark); leg.position.set(lx, 0.22, 0); leg.castShadow = true; g.add(leg); + } + for (let p = 0; p < 3; p++) { + if (cfg.quebrado === 'sem-tabua' && p === 1) continue; + const plank = new THREE.Mesh(boxGeometry(1.8, 0.06, 0.16), MAT.wood); + plank.position.set(0, 0.47, -0.18 + p * 0.18); plank.castShadow = true; + if (cfg.quebrado === 'torta' && p === 2) { plank.rotation.z = 0.16; plank.position.y = 0.43; } + g.add(plank); + } + for (let p = 0; p < 2; p++) { + if (cfg.quebrado === 'sem-encosto' && p === 1) continue; + const back = new THREE.Mesh(boxGeometry(1.8, 0.15, 0.05), MAT.wood); + back.position.set(0, 0.78 + p * 0.22, -0.32); back.rotation.x = -0.14; back.castShadow = true; g.add(back); + } + colliders.push({ minX: cfg.x - 0.95, maxX: cfg.x + 0.95, minY: 0, maxY: 0.52, minZ: cfg.z - 0.35, maxZ: cfg.z + 0.35 }); + occluders.push(g.children[2]); + }); + + /* Vegetação: tronco com colisor fino, copa/arbusto sem colisor (o grafo nasce + dos colisores no fim do build); candidatos a ≥0,9 m do reticulado de nós. */ + const jitterGeo = (geo, amp, seed) => { + let s = seed >>> 0; + const rnd = () => ((s = (s * 1664525 + 1013904223) >>> 0) / 4294967296); + const pos = geo.attributes.position; + for (let i = 0; i < pos.count; i++) pos.setXYZ(i, pos.getX(i) + (rnd() - 0.5) * amp, pos.getY(i) + (rnd() - 0.5) * amp, pos.getZ(i) + (rnd() - 0.5) * amp); + geo.computeVertexNormals(); + return geo; + }; + const trunkPain = new THREE.CylinderGeometry(0.16, 0.24, 2.4, 7).translate(0, 1.2, 0); + const canopyPain = jitterGeo(new THREE.SphereGeometry(1.9, 10, 8).scale(1, 0.82, 1).translate(0, 3.05, 0), 0.34, 7101); + const trunkPalm = new THREE.CylinderGeometry(0.11, 0.17, 4.6, 7).translate(0, 2.3, 0); + const crownPalm = mergeParts((() => { + const parts = []; + for (let i = 0; i < 7; i++) { + const f = new THREE.BoxGeometry(0.16, 0.05, 2.1).translate(0, 0, 0.85); + f.rotateX(0.52); f.rotateY(i * Math.PI * 2 / 7); f.translate(0, 4.72, 0); + parts.push(f); + } + parts.push(new THREE.SphereGeometry(0.22, 7, 5).translate(0, 4.68, 0)); + return parts; + })()); + const shrubGeo = jitterGeo(new THREE.SphereGeometry(0.7, 8, 6).scale(1, 0.72, 1).translate(0, 0.42, 0), 0.22, 4402); + + const CLEAR_RECT = [ + [-6.8, -HALF_Z, 6.8, HALF_Z], [-HALF_X, -5.8, HALF_X, 5.8], + [-13.8, 31.5, 13.8, HALF_Z], [-13.8, -HALF_Z, 13.8, -31.5], + ]; + const CLEAR_CIRC = [ + [18, -33, 4.5], [-18, 33, 4.5], [0, 10, 4], + [WHEEL_X, 0, 6.5], [24, 0, 7.5], [CORETO.x, CORETO.z, 5.8], [ESTACAO.x, ESTACAO.z, 4], + [-19, 29, 4.8], [-19, -29, 4.8], + [-13, -19, 4.2], [13, 19, 4.2], [13, -19, 4.2], [-13, 19, 4.2], + ]; + const NODE0_X = -HALF_X + 2, NODE0_Z = -HALF_Z + 2, NSTEP = 3.2; + const pertoDeNo = (x, z) => { + const lx = NODE0_X + Math.round((x - NODE0_X) / NSTEP) * NSTEP; + const lz = NODE0_Z + Math.round((z - NODE0_Z) / NSTEP) * NSTEP; + return Math.hypot(x - lx, z - lz) < 0.9; + }; + const emColisor = (x, z, pad) => colliders.some(c => x > c.minX - pad && x < c.maxX + pad && z > c.minZ - pad && z < c.maxZ + pad); + const pertoDoTrilho = (x, z) => { + for (let i = 0; i < ringSamples.length; i += 4) if (Math.hypot(x - ringSamples[i].x, z - ringSamples[i].z) < 1.3) return true; + return false; + }; + const livreVeg = (x, z, pad = 0.7) => + !pertoDeNo(x, z) && !emColisor(x, z, pad) && !pertoDoTrilho(x, z) + && !CLEAR_RECT.some(r => x > r[0] && x < r[2] && z > r[1] && z < r[3]) + && !CLEAR_CIRC.some(c => Math.hypot(x - c[0], z - c[1]) < c[2]); + + let vegSeed = 20260825; + const vrnd = () => ((vegSeed = (vegSeed * 1664525 + 1013904223) >>> 0) / 4294967296); + const candidatos = []; + for (let z = -38.5; z <= 38.5; z += 3.4) { + candidatos.push([-29.5 + vrnd() * 0.8, z + (vrnd() - 0.5) * 1.4]); + candidatos.push([29.5 - vrnd() * 0.8, z + (vrnd() - 0.5) * 1.4]); + } + for (let x = -28.5; x <= 28.5; x += 3.4) { + candidatos.push([x + (vrnd() - 0.5) * 1.4, -39.5 + vrnd() * 0.9]); + candidatos.push([x + (vrnd() - 0.5) * 1.4, 39.5 - vrnd() * 0.9]); + } + for (let z = -30; z <= 30; z += 6.8) { + candidatos.push([-18.6, z + (vrnd() - 0.5) * 2.2]); candidatos.push([18.6, z + (vrnd() - 0.5) * 2.2]); + candidatos.push([-25.6, z + 3.4 + (vrnd() - 0.5) * 2.2]); candidatos.push([25.6, z + 3.4 + (vrnd() - 0.5) * 2.2]); + } + for (const [cx, cz] of [[-27, -36], [27, -36], [-27, 36], [27, 36]]) + for (let i = 0; i < 4; i++) candidatos.push([cx + (vrnd() - 0.5) * 5.5, cz + (vrnd() - 0.5) * 4.5]); + + const batPainT = new InstBatch({ name: 'parque-arvores-paineira' }); + const batPainC = new InstBatch({ name: 'parque-copas-paineira' }); + const batPalmT = new InstBatch({ name: 'parque-arvores-palmeira' }); + const batPalmC = new InstBatch({ name: 'parque-copas-palmeira' }); + const batArbA = new InstBatch({ name: 'parque-arbustos-buxinho' }); + const batArbB = new InstBatch({ name: 'parque-arbustos-flor' }); + const CORES_COPA = [0x2f8f3f, 0x57a83e, 0x3f9e58, 0x6fae4a, 0x2a7a46]; + const dummy = new THREE.Object3D(); + const plantadas = []; + let nArvore = 0; + for (const [x, z] of candidatos) { + if (!livreVeg(x, z)) continue; + if (plantadas.some(p => Math.hypot(x - p[0], z - p[1]) < 2.3)) continue; + plantadas.push([x, z]); + const palmeira = nArvore % 4 === 3; + dummy.position.set(x, 0, z); + dummy.rotation.set(palmeira ? (vrnd() - 0.5) * 0.09 : 0, vrnd() * Math.PI * 2, palmeira ? (vrnd() - 0.5) * 0.09 : 0); + const s = 0.85 + vrnd() * 0.45; + dummy.scale.set(s, s * (0.9 + vrnd() * 0.3), s); dummy.updateMatrix(); + const cor = CORES_COPA[Math.floor(vrnd() * CORES_COPA.length)]; + if (palmeira) { batPalmT.add(trunkPalm, MAT.trunk, dummy.matrix); batPalmC.add(crownPalm, MAT.leaf, dummy.matrix, cor); } + else { batPainT.add(trunkPain, MAT.trunk, dummy.matrix); batPainC.add(canopyPain, MAT.leaf, dummy.matrix, cor); } + const hTronco = (palmeira ? 4.6 : 2.4) * dummy.scale.y; + colliders.push({ minX: x - 0.2, maxX: x + 0.2, minY: 0, maxY: Math.min(hTronco, 2.4), minZ: z - 0.2, maxZ: z + 0.2 }); + nArvore++; + if (vrnd() < 0.9) { + const aa = vrnd() * Math.PI * 2, dd = 1.5 + vrnd() * 0.9, sx = x + Math.cos(aa) * dd, sz = z + Math.sin(aa) * dd; + if (livreVeg(sx, sz, 0.3)) { + dummy.position.set(sx, 0, sz); dummy.rotation.set(0, vrnd() * Math.PI * 2, 0); + const ss = 0.7 + vrnd() * 0.8; dummy.scale.set(ss, ss * (0.8 + vrnd() * 0.4), ss); dummy.updateMatrix(); + (vrnd() < 0.72 ? batArbA : batArbB).add(shrubGeo, MAT.leaf, dummy.matrix, vrnd() < 0.72 ? 0x2c7a38 : 0x9e6fb8); + } + } + } + // Faixa de arbustos rente à cerca viva, fechando o rodapé da sebe. + for (let z = -39; z <= 39; z += 2.4) for (const sx of [-30.4, 30.4]) { + const x = sx + (vrnd() - 0.5) * 0.7, zz = z + (vrnd() - 0.5) * 1.1; + if (!livreVeg(x, zz, 0.3)) continue; + dummy.position.set(x, 0, zz); dummy.rotation.set(0, vrnd() * Math.PI * 2, 0); + const ss = 0.75 + vrnd() * 0.7; dummy.scale.set(ss, ss * (0.8 + vrnd() * 0.4), ss); dummy.updateMatrix(); + (vrnd() < 0.8 ? batArbA : batArbB).add(shrubGeo, MAT.leaf, dummy.matrix, vrnd() < 0.8 ? 0x2c7a38 : 0x9e6fb8); + } + for (let x = -28; x <= 28; x += 2.4) for (const sz of [-40.3, 40.3]) { + const z = sz + (vrnd() - 0.5) * 0.7, xx = x + (vrnd() - 0.5) * 1.1; + if (!livreVeg(xx, z, 0.3)) continue; + dummy.position.set(xx, 0, z); dummy.rotation.set(0, vrnd() * Math.PI * 2, 0); + const ss = 0.75 + vrnd() * 0.7; dummy.scale.set(ss, ss * (0.8 + vrnd() * 0.4), ss); dummy.updateMatrix(); + (vrnd() < 0.8 ? batArbA : batArbB).add(shrubGeo, MAT.leaf, dummy.matrix, vrnd() < 0.8 ? 0x2c7a38 : 0x9e6fb8); + } + occluders.push(...batPainT.build(root)); + batPainC.build(root); + occluders.push(...batPalmT.build(root)); + batPalmC.build(root); + batArbA.build(root); + batArbB.build(root); const GM = { black: lam({ color: 0x202735 }), steel: lam({ color: 0xaab4c0 }), wood: MAT.wood, green: lam({ color: 0x315b43 }) }; const gbox = (w, h, d, mat, x, y, z) => { const mesh = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat); mesh.position.set(x, y, z); return mesh; }; @@ -354,11 +743,6 @@ export function buildParque(scene, T) { return [fromIdx]; } - const hemi = new THREE.HemisphereLight(0xdaf5ff, 0x71a95b, 1.35); scene.add(hemi); - const sun = new THREE.DirectionalLight(0xfff3d2, 1.35); sun.position.set(-24, 44, -18); sun.castShadow = true; - sun.shadow.mapSize.set(2048, 2048); sun.shadow.camera.left = -42; sun.shadow.camera.right = 42; sun.shadow.camera.top = 50; sun.shadow.camera.bottom = -50; sun.shadow.camera.far = 150; sun.shadow.bias = -0.0004; scene.add(sun); - const fill = new THREE.DirectionalLight(0xbde7ff, 0.45); fill.position.set(30, 22, 28); scene.add(fill); - function update(dt, time) { SURFACE.water.offset.x = time * 0.018; SURFACE.water.offset.y = time * 0.009; diff --git a/public/js/mapprops.js b/public/js/mapprops.js index 1983597c..87ee5749 100644 --- a/public/js/mapprops.js +++ b/public/js/mapprops.js @@ -295,7 +295,7 @@ export class PropBatch { cadeira, guarda-sol, laje). Mesma ideia do PropBatch, sem glTF: o mapa registra matriz + cor e no fim sai 1 draw call por (geometria, material, bloco). */ export class InstBatch { - constructor(opts = {}) { this.bucket = opts.bucket || 0; this.by = new Map(); } + constructor(opts = {}) { this.bucket = opts.bucket || 0; this.by = new Map(); this.name = opts.name || ''; } /* geo/mat DEVEM ser sempre as mesmas referencias para o mesmo grupo. */ add(geo, mat, mtxOrObj, color = null, opts = {}) { const m = mtxOrObj.isObject3D ? (mtxOrObj.updateMatrix(), mtxOrObj.matrix.clone()) : mtxOrObj.clone(); @@ -307,6 +307,7 @@ export class InstBatch { g.list.push(m); g.cols.push(color); } build(root) { + const out = []; for (const g of this.by.values()) { const im = new THREE.InstancedMesh(g.geo, g.mat, g.list.length); let anyCol = false; @@ -319,9 +320,11 @@ export class InstBatch { im.castShadow = g.cast; im.receiveShadow = true; im.frustumCulled = PROP_CULL; im.computeBoundingSphere(); - root.add(im); + if (this.name) im.name = this.name; + root.add(im); out.push(im); } this.by.clear(); + return out; } } diff --git a/public/js/maps.js b/public/js/maps.js index cb3b74c6..b40f2099 100644 --- a/public/js/maps.js +++ b/public/js/maps.js @@ -13,7 +13,7 @@ import { buildObras, OBRAS_PROPS } from './map_obras.js'; import { buildPosto, POSTO_PROPS } from './map_posto.js'; import { buildUpa, UPA_PROPS } from './map_upa.js'; import { buildAtacadao, ATACADAO_PROPS } from './map_atacadao.js'; -import { buildParque } from './map_parque.js'; +import { buildParque, PARQUE_PROPS, PARQUE_AMBIENCE } from './map_parque.js'; import { buildVelhoOeste } from './map_velho_oeste.js'; import { buildPenitenciaria } from './map_penitenciaria.js'; @@ -89,7 +89,7 @@ export const MAPS = { // por portas de verdade na fachada. Gôndolas reais (gondola_mercado/eletro), caixas, doca, e um // bairro/skyline em volta. A treta é o preço absurdo. Simétrico funcional, A* pelos corredores. atacadao_treta: { name: 'Atacadão da Treta', build: buildAtacadao, props: ATACADAO_PROPS, ctfMode: true }, - parque_treta: { name: 'Parque da Treta', build: buildParque, ctfMode: true }, + parque_treta: { name: 'Parque da Treta', build: buildParque, props: PARQUE_PROPS, ambience: PARQUE_AMBIENCE, ctfMode: true }, velho_oeste: { name: 'Velho Oeste da Treta', build: buildVelhoOeste, ctfMode: true }, penitenciaria: { name: 'Penitenciária da Treta', build: buildPenitenciaria, ctfMode: true }, }; diff --git a/public/models/props/FONTE.md b/public/models/props/FONTE.md index 0588565d..05b93ed6 100644 --- a/public/models/props/FONTE.md +++ b/public/models/props/FONTE.md @@ -174,3 +174,35 @@ 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`. +- `parque_coreto.glb` — "Festive Green Coreto", coreto octogonal de praça com + colunas de ferro branco, telhado de zinco verde e bandeirolas juninas. 4.742 + tris, 1 primitiva, texturas WebP 1024. Mint text-to-3D, gerado 25/08/2026 para + o rebuild USANTOS do parque_treta (pedido do dono: mapas usantos "não podem + parecer low poly"). Chat: . + Registro: `coreto-praca-parque`. Otimizado por `tools/optimize-props.mjs` + (dedup + webp 1024 + prune, 1.107 KB → 448 KB). No mapa, substitui o coreto + procedural quando carrega (fallback idêntico em colisores). +- `roda_gigante.glb` — "Ferris Wheel", roda-gigante completa (aro duplo, raios, + 14 gôndolas, pernas em A, plataforma). 3.913 tris. Mint text-to-3D, kit + "parque" (26/08/2026, pack th783j3d4kq2saf84wyv3kfpkh8d6fmz). Chat: + . Registro: + `roda-gigante-parque`. Modelo-fonte do split abaixo; o mapa não o carrega. +- `roda_gigante_roda.glb` — parte ROTATIVA do split (aro duplo + raios + + gôndolas). 2.871 tris. Split por componentes conexos em Blender headless + (`tools/blender-split-roda-gigante.py`), verificado por render (composite + idêntico ao original). Registro: `roda-gigante-roda-parque`. No mapa, filha + do grupo `roda-gigante` — gira com a rotação procedural do update. +- `roda_gigante_base.glb` — parte ESTÁTICA do split (pernas A, pés, plataforma, + cabine do operador). 1.042 tris, mesma origem e script. Registro: + `roda-gigante-base-parque`. Pés em y=0, mesma escala da parte rotativa. +- `carrossel.glb` — "Carousel", carrossel com cavalos e cobertura. 4.419 tris, + mesmo kit/chat. Registro: `carrossel-parque`. Filho do grupo + `carrossel-giratorio` (gira junto); substitui plataforma/cavalos/teto + procedurais no browser. +- `predio_artdeco.glb` — "Art Deco Building", prédio de entorno. 4.297 tris, + mesmo kit/chat. Registro: `predio-artdeco-entorno-parque`. 8 instâncias fora + dos bounds jogáveis fechando o quarteirão, rotações/alturas variadas. +- `barraca_quermesse.glb` — "Quermesse Tent", barraca de quermesse. 4.419 + tris, mesmo kit/chat. Registro: `barraca-quermesse-parque`. Substitui os 4 + quiosques-caixa (PIPOCA/ALGODÃO DOCE/PESCARIA/ARGOLA); placas signTexture e + colisores procedurais mantidos. diff --git a/public/models/props/barraca_quermesse.glb b/public/models/props/barraca_quermesse.glb new file mode 100644 index 00000000..ad8a0a81 Binary files /dev/null and b/public/models/props/barraca_quermesse.glb differ diff --git a/public/models/props/carrossel.glb b/public/models/props/carrossel.glb new file mode 100644 index 00000000..f134bcb5 Binary files /dev/null and b/public/models/props/carrossel.glb differ diff --git a/public/models/props/parque_coreto.glb b/public/models/props/parque_coreto.glb new file mode 100644 index 00000000..dc98883d Binary files /dev/null and b/public/models/props/parque_coreto.glb differ diff --git a/public/models/props/predio_artdeco.glb b/public/models/props/predio_artdeco.glb new file mode 100644 index 00000000..f1341ad2 Binary files /dev/null and b/public/models/props/predio_artdeco.glb differ diff --git a/public/models/props/roda_gigante.glb b/public/models/props/roda_gigante.glb new file mode 100644 index 00000000..3b52a7d5 Binary files /dev/null and b/public/models/props/roda_gigante.glb differ diff --git a/public/models/props/roda_gigante_base.glb b/public/models/props/roda_gigante_base.glb new file mode 100644 index 00000000..82ad3069 Binary files /dev/null and b/public/models/props/roda_gigante_base.glb differ diff --git a/public/models/props/roda_gigante_roda.glb b/public/models/props/roda_gigante_roda.glb new file mode 100644 index 00000000..69253657 Binary files /dev/null and b/public/models/props/roda_gigante_roda.glb differ diff --git a/tools/blender-split-roda-gigante.py b/tools/blender-split-roda-gigante.py new file mode 100644 index 00000000..ec9c5321 --- /dev/null +++ b/tools/blender-split-roda-gigante.py @@ -0,0 +1,73 @@ +# Split da roda_gigante.glb (Mint) em parte ROTATIVA e ESTATICA por componente +# conexo, para a roda girar no grupo 'roda-gigante' do parque_treta sem levar +# as pernas junto. Uso: Blender -b -P tools/blender-split-roda-gigante.py -- \ +# +# Regras (medidas por dump de componentes — cubo em (0, 0.065), aro raio ~0,44): +# perna = atravessa o aro E chega aos pés (miny<-0,47) | fragmento abaixo do +# fundo do aro dentro do círculo sem alcançar o aro | laje achatada larga +# (deck/plataforma) | haste fina em z no fundo | laje-fini vertical (dz>0,35). +# rotativa = resto com cy>=-0,42 (gôndolas de baixo pendem além do aro), +# dist<=0,62 e (miolo |z|<=0,14 ou faixa do aro dist>=0,36 com |z|<=0,30). +# Verificação visual (lei 4): renders /tmp/r2-parque/v10_*.png — roda completa +# (aro duplo+raios+14 gôndolas), base limpa (pernas A, plataforma, cabine), +# composite idêntico ao modelo original. +import bpy, bmesh, math, sys +argv = sys.argv[sys.argv.index('--') + 1:] +SRC, OUT_RODA, OUT_BASE = argv[0], argv[1], argv[2] +HX, HY = 0.0, 0.108 + +bpy.ops.wm.read_factory_settings(use_empty=True) +bpy.ops.import_scene.gltf(filepath=SRC) +obj = [o for o in bpy.context.scene.objects if o.type == 'MESH'][0] +mw = obj.matrix_world +bpy.context.view_layer.objects.active = obj +bpy.ops.object.mode_set(mode='EDIT') +bm = bmesh.from_edit_mesh(obj.data) +bm.faces.ensure_lookup_table() +comp = {} +def componente(seed): + stack=[seed]; grupo=[]; comp[seed.index]=True + while stack: + f=stack.pop(); grupo.append(f) + for e in f.edges: + for lf in e.link_faces: + if lf.index not in comp: comp[lf.index]=True; stack.append(lf) + return grupo + +nrot = nest = faces_rot = 0 +for f in bm.faces: + if f.index in comp: continue + g = componente(f) + xs=[]; ys=[]; zs=[]; ds=[] + for face in g: + for v in face.verts: + c = mw @ v.co + gx, gy, gz = c.x, c.z, -c.y + xs.append(gx); ys.append(gy); zs.append(gz); ds.append(math.hypot(gx-HX, gy-HY)) + cx=sum(xs)/len(xs); cy=sum(ys)/len(ys); cz=sum(zs)/len(zs) + dist=math.hypot(cx-HX, cy-HY) + miny=min(ys); dmin=min(ds); dmax=max(ds) + dx=max(xs)-min(xs); dy=max(ys)-min(ys); dz=max(zs)-min(zs) + perna = ((dmin < 0.30 and dmax > 0.47 and miny < -0.47) + or (miny < -0.355 and dist < 0.38 and abs(cz) < 0.25 and dmax < 0.47) + or (dy < 0.15 and (dx > 0.25 or dz > 0.25)) + or (dy < 0.05 and dz > 0.30 and cy < -0.30) + or (dz > 0.35 and dx < 0.1)) + rot = (not perna and cy >= -0.42 and dist <= 0.62 + and (abs(cz) <= 0.14 or (dist >= 0.36 and abs(cz) <= 0.30))) + for face in g: face.select_set(rot) + if rot: nrot += 1; faces_rot += len(g) + else: nest += 1 +print(f'COMPONENTES rot={nrot} est={nest} faces_rot={faces_rot}') +bmesh.update_edit_mesh(obj.data) +bpy.ops.mesh.separate(type='SELECTED') +bpy.ops.object.mode_set(mode='OBJECT') +roda = [o for o in bpy.context.scene.objects if o.type == 'MESH' and o.select_get() and o != obj] +assert len(roda) == 1, f'separacao inesperada: {len(roda)}' +roda = roda[0]; base = obj +roda.name = 'roda_gigante_roda'; base.name = 'roda_gigante_base' +for o, path in [(roda, OUT_RODA), (base, OUT_BASE)]: + bpy.ops.object.select_all(action='DESELECT') + o.select_set(True); bpy.context.view_layer.objects.active = o + bpy.ops.export_scene.gltf(filepath=path, export_format='GLB', use_selection=True) + print('EXPORTED', path, len(o.data.polygons)) diff --git a/tools/eval/look-check.mjs b/tools/eval/look-check.mjs index 6fbacd64..6f639c90 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', 'parque_treta']; // rollout: +parque_treta (frente USANTOS, 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..902bb821 100644 --- a/tools/eval/look-horizonte.json +++ b/tools/eval/look-horizonte.json @@ -8,6 +8,15 @@ "n": 19596, "zenite": "6595bf" }, + "sky_parque.webp": { + "banda": [ + 498, + 510 + ], + "horizonte": "7b739a", + "n": 22620, + "zenite": "193660" + }, "sky_rj.webp": { "banda": [ 429, diff --git a/tools/eval/map_check.json b/tools/eval/map_check.json index f5986f4b..4ccb26bd 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-26T04:26:03.766Z", "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/parque-vida-check.mjs b/tools/eval/parque-vida-check.mjs new file mode 100644 index 00000000..66ecf712 --- /dev/null +++ b/tools/eval/parque-vida-check.mjs @@ -0,0 +1,244 @@ +/* ============================================================================ + parque-vida-check.mjs — O PARQUE DA TRETA NÃO PODE PARECER LOW POLY (frente USANTOS). + ---------------------------------------------------------------------------- + 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." + "Não parecer low poly" é mensurável sem abrir o jogo: o parque ATUAL é uma + cerca viva, 4 quiosques-caixa e um gramado chapado — tudo primitiva com 8 + texturas de canvas e ZERO mobiliário de parque (não tem banco, lixeira, + coreto nem árvore). 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) + PV1 mobiliário de parque NOMEADO presente (lição 14: referência a conteúdo + é por NOME): parque-coreto (≥1), parque-lixeira-* (≥6), parque-banco-* + (≥6), parque-trilho-* (≥1 linha de trilho) e dormentes instanciados + parque-dormente* (≥100 — o circuito do trenzinho mede ~210 m ÷ + espaçamento 0,9 m ≈ 230 dormentes; 100 é metade do medido, folga para + ajuste de traçado sem esvaziar a cláusula). + PV2 vegetação densa instanciada: grupos parque-arvores-* ≥ 40 instâncias + (troncos = 1 por árvore; copas moram em parque-copas-* e NÃO contam + para não dobrar a medição) e parque-arbustos-* ≥ 60. Procedência: + bounds jogáveis 62,4×82,4 m = 5.142 m²; 40 árvores ≈ 1 por 128 m² — + o desenho do rebuild (cortina no perímetro interno + maciços de canto + + alamedas) mede 44. Limiar proposto na missão e conferido no layout. + PV3 variedade de superfície: nº de material.map.name distintos na cena + ≥ 16. Medido no estado atual (25/08/2026): 8 (parque-concrete, -grass, + -hedge, -metal, -paint, -tiles, -water, -wood). O rebuild adiciona + folha, casca, terra, lona, telhado, lata, pedra e asfalto = 16. + PV4 nenhum quadrante deserto: grade 4×4 sobre os bounds; quadrante com nó + de waypoint precisa de peça de cobertura (colisor 0,4 ≤ h ≤ 2,5 m) — + nenhum zerado nem abaixo de 0,35× a mediana. É o espírito da MAP5 + (invariants.mjs ~1961) aplicado ao mapa que está NASCENDO cheio, não + ao loja_h. Medido no estado atual: mín 1, mediana 2 — passa; a + cláusula existe para o rebuild não criar deserto novo. + PV5 horizonte próprio: scene.userData.skyUrl gravado pelo setMapSky (a + régua lê o USO, não a declaração — BUG-02) E entrada LOOK.parque_treta + em public/js/look.js. + PV6 moldes 3D no lugar das primitivas (feedback do dono, 26/08/2026: + "parque da treta ainda esta low poly, predios low poly, brinquedos + low poly"). Como no arnês node o GLB não baixa, o mapa cria GRUPOS + WRAPPER NOMEADOS nos dois mundos (o GLB entra dentro no browser; em + node ficam vazios). Mede: ≥ 14 wrappers parque-molde-* no mundo + buildado — contagem real do build: 8 prédios + 4 barracas + roda + + roda-base + carrossel = 15 (public/js/map_parque.js, blocos + PREDIOS/kiosk/roda-gigante/carrossel); PARQUE_PROPS contém os 5 ids + novos; e cada models/props/.glb existe em disco. O mutante + --mutante=sem-moldes filtra os wrappers do traverse e esvazia os ids + em memória → PV6 tem que ficar vermelha. + + FALHA = NÃO SABER MEDIR (lição 5): build que lança, waypoints ausentes ou + quadrante impossível de classificar reprovam com mensagem de conserto. + + USO + node tools/eval/parque-vida-check.mjs + node tools/eval/parque-vida-check.mjs --mutante=sem-coreto # PV1 + node tools/eval/parque-vida-check.mjs --mutante=sem-vegetacao # PV2 + node tools/eval/parque-vida-check.mjs --mutante=sem-variedade # PV3 + node tools/eval/parque-vida-check.mjs --mutante=sem-moldes # PV6 + ============================================================================ */ +import { THREE, MAPS, initTextures } from './harness.mjs'; +import { LOOK } from '../../public/js/look.js'; +import { PARQUE_PROPS } from '../../public/js/map_parque.js'; +import { existsSync } from 'node:fs'; + +const MUTANTE = (process.argv.find((a) => a.startsWith('--mutante=')) || '').split('=')[1] || null; +const conhecidos = new Set(['sem-coreto', 'sem-vegetacao', 'sem-variedade', 'sem-moldes']); +if (MUTANTE && !conhecidos.has(MUTANTE)) throw new Error(`mutante desconhecido: ${MUTANTE}`); + +const MIN_LIXEIRAS = 6, MIN_BANCOS = 6, MIN_DORMENTES = 100; +const MIN_ARVORES = 40, MIN_ARBUSTOS = 60, MIN_TEXTURAS = 16; +const MIN_MOLDES = 14; +const MOLDE_IDS = ['roda_gigante_roda', 'roda_gigante_base', 'carrossel', 'barraca_quermesse', 'predio_artdeco']; + +/* instâncias sob um prefixo de nome: InstancedMesh conta `count`, o resto conta 1 + (um Grupo nomeado parque-banco-3 é UM banco, suas tábuas não têm nome) */ +function contaPorPrefixo(root, prefixo) { + let n = 0; + root.traverse((o) => { if (o.name && o.name.startsWith(prefixo)) n += o.isInstancedMesh ? o.count : 1; }); + return n; +} + +const T = await initTextures(); +const scene = new THREE.Scene(); +let W = null, erroBuild = null; +try { W = MAPS.parque_treta.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 || !W?.waypoints?.nodes || !W?.bounds) { + put('PV0', false, `não sei medir: build do parque ${erroBuild ? `lançou "${erroBuild}"` : 'devolveu mundo sem root/waypoints/bounds'} — conserte o build antes da régua`); + console.log('\nPARQUE-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-coreto') { + const coreto = W.root.getObjectByName('parque-coreto'); + mutanteAplicou = !!coreto; + if (coreto) coreto.parent.remove(coreto); +} else if (MUTANTE === 'sem-vegetacao') { + const alvos = []; + W.root.traverse((o) => { if (/^parque-(arvores|arbustos|copas)-/.test(o.name || '')) alvos.push(o); }); + mutanteAplicou = alvos.length > 0; + for (const o of alvos) o.parent.remove(o); +} else if (MUTANTE === 'sem-moldes') { + const alvos = []; + W.root.traverse((o) => { if ((o.name || '').startsWith('parque-molde-')) alvos.push(o); }); + PARQUE_PROPS.length = 0; + mutanteAplicou = alvos.length > 0; + for (const o of alvos) o.parent.remove(o); +} else if (MUTANTE === 'sem-variedade') { + let primeira = null; + W.root.traverse((o) => { + if (!o.isMesh) return; + for (const m of Array.isArray(o.material) ? o.material : [o.material]) { + if (!m?.map) continue; + if (!primeira) primeira = m.map; + } + }); + mutanteAplicou = !!primeira; + if (primeira) W.root.traverse((o) => { + if (!o.isMesh) return; + for (const m of Array.isArray(o.material) ? o.material : [o.material]) if (m?.map) m.map = primeira; + }); +} +if (MUTANTE && !mutanteAplicou) { + console.error(`MUTANTE NÃO APLICOU: ${MUTANTE} — a régua não mede o que o mutante quebra`); + process.exit(1); +} + +/* ---- PV1 mobiliário nomeado ---- */ +{ + const coreto = contaPorPrefixo(W.root, 'parque-coreto'); + const lixeiras = contaPorPrefixo(W.root, 'parque-lixeira-'); + const bancos = contaPorPrefixo(W.root, 'parque-banco-'); + const trilhos = contaPorPrefixo(W.root, 'parque-trilho-'); + const dormentes = contaPorPrefixo(W.root, 'parque-dormente'); + const falta = []; + if (coreto < 1) falta.push('coreto 0/1'); + if (lixeiras < MIN_LIXEIRAS) falta.push(`lixeiras ${lixeiras}/${MIN_LIXEIRAS}`); + if (bancos < MIN_BANCOS) falta.push(`bancos ${bancos}/${MIN_BANCOS}`); + if (trilhos < 1) falta.push('trilho 0/1'); + if (dormentes < MIN_DORMENTES) falta.push(`dormentes ${dormentes}/${MIN_DORMENTES}`); + put('PV1', !falta.length, + falta.length + ? `faltam: ${falta.join(' · ')} — nomeie o mobiliário no build (parque-coreto, parque-lixeira-*, parque-banco-*, parque-trilho-*, parque-dormente*)` + : `coreto ${coreto} · lixeiras ${lixeiras} · bancos ${bancos} · trilhos ${trilhos} · dormentes ${dormentes}`); +} + +/* ---- PV2 vegetação densa ---- */ +{ + const arvores = contaPorPrefixo(W.root, 'parque-arvores-'); + const arbustos = contaPorPrefixo(W.root, 'parque-arbustos-'); + const falta = []; + if (arvores < MIN_ARVORES) falta.push(`árvores ${arvores}/${MIN_ARVORES}`); + if (arbustos < MIN_ARBUSTOS) falta.push(`arbustos ${arbustos}/${MIN_ARBUSTOS}`); + put('PV2', !falta.length, + falta.length + ? `${falta.join(' · ')} — instancie a vegetação (InstBatch parque-arvores-*/parque-arbustos-*): 5.142 m² de parque sem árvore é o "low poly" que o dono nomeou` + : `árvores ${arvores} · arbustos ${arbustos} instanciados`); +} + +/* ---- PV3 variedade 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('PV3', nomes.size >= MIN_TEXTURAS, + nomes.size >= MIN_TEXTURAS + ? `${nomes.size} texturas distintas em uso` + : `${nomes.size}/${MIN_TEXTURAS} texturas distintas (${[...nomes].sort().join(', ') || 'nenhuma'}) — primitiva com 8 canvas é a cara low poly; crie superfícies novas (folha, casca, terra, lona…)`); +} + +/* ---- PV4 quadrantes sem deserto ---- */ +{ + const B = W.bounds, QX = 4, QZ = 4; + const cw = (B.maxX - B.minX) / QX, ch = (B.maxZ - B.minZ) / QZ; + const clamp = (v, n) => Math.max(0, Math.min(n - 1, v)); + const qi = (x, z) => clamp(Math.floor((x - B.minX) / cw), QX) + QX * clamp(Math.floor((z - B.minZ) / ch), QZ); + const grid = Array.from({ length: QX * QZ }, () => ({ cover: 0, nodes: 0 })); + for (const c of W.colliders || []) { + const h = c.maxY - c.minY; + if (h < 0.4 || h > 2.5) continue; + const cx = (c.minX + c.maxX) / 2, cz = (c.minZ + c.maxZ) / 2; + if (cx < B.minX || cx > B.maxX || cz < B.minZ || cz > B.maxZ) continue; + grid[qi(cx, cz)].cover++; + } + for (const n of W.waypoints.nodes) grid[qi(n.x, n.z)].nodes++; + const andaveis = grid.map((g, i) => ({ ...g, i })).filter((g) => g.nodes > 0); + if (!andaveis.length) { + put('PV4', false, 'não sei medir: nenhum quadrante andável — grafo de waypoints vazio?'); + } else { + const mediana = andaveis.map((g) => g.cover).sort((a, b) => a - b)[Math.floor(andaveis.length / 2)]; + const ruins = andaveis.filter((g) => g.cover < 1 || g.cover < 0.35 * mediana); + put('PV4', !ruins.length, + ruins.length + ? `quadrantes desertos: ${ruins.map((g) => `q${g.i} cover=${g.cover}`).join(' · ')} (mediana ${mediana}) — espalhe cobertura: banco, lixeira, tronco, floreira` + : `${andaveis.length} quadrantes andáveis, cobertura mín ${Math.min(...andaveis.map((g) => g.cover))} · mediana ${mediana}`); + } +} + +/* ---- PV5 horizonte próprio ---- */ +{ + const sky = scene.userData.skyUrl; + const look = LOOK.parque_treta; + const falta = []; + if (!sky) falta.push('scene.userData.skyUrl ausente (o mapa não passou pelo applyLook/setMapSky)'); + if (!look) falta.push('LOOK.parque_treta ausente em public/js/look.js (APPEND na tabela)'); + put('PV5', !falta.length, falta.length ? falta.join(' · ') : `sky ${sky} · fog do look ${look.neblina ? 'ok' : '?'}`); +} + +/* ---- PV6 moldes 3D nomeados ---- */ +{ + const moldes = contaPorPrefixo(W.root, 'parque-molde-'); + const idsFaltando = MOLDE_IDS.filter((id) => !PARQUE_PROPS.includes(id)); + const glbFaltando = MOLDE_IDS.filter((id) => !existsSync(`public/models/props/${id}.glb`)); + const falta = []; + if (moldes < MIN_MOLDES) falta.push(`wrappers parque-molde-* ${moldes}/${MIN_MOLDES}`); + if (idsFaltando.length) falta.push(`fora de PARQUE_PROPS: ${idsFaltando.join(', ')}`); + if (glbFaltando.length) falta.push(`GLB ausente em disco: ${glbFaltando.join(', ')}`); + put('PV6', !falta.length, + falta.length + ? `${falta.join(' · ')} — primitiva no lugar do molde é o "low poly" que o dono nomeou de novo: crie os wrappers parque-molde-* e registre os ids em PARQUE_PROPS` + : `${moldes} wrappers parque-molde-* · ${MOLDE_IDS.length} ids em PARQUE_PROPS · GLBs em disco`); +} + +/* ---- placar e veredito dos mutantes ---- */ +const vermelhas = clausulas.filter((c) => !c.ok); +const ALVO = { 'sem-coreto': 'PV1', 'sem-vegetacao': 'PV2', 'sem-variedade': 'PV3', 'sem-moldes': 'PV6' }; +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(`\nPARQUE-VIDA ${vermelhas.length ? `VERMELHA · ${vermelhas.map((c) => c.id).join(', ')}` : 'ok · PV1-PV6'}`); +process.exit(vermelhas.length ? 1 : 0);