diff --git a/ARCH.generated.md b/ARCH.generated.md index 725b84f0..8333cccd 100644 --- a/ARCH.generated.md +++ b/ARCH.generated.md @@ -8,7 +8,7 @@ Números atuais das zonas, do quality gate e do `package.json`: | Zona | O que é | Tamanho medido | Regra | |---|---|---|---| -| `public/` | o **jogo** | 44 arquivos `.js`, 32.001 linhas · Three.js `r160` vendorizado | ES modules servidos crus, **zero build**, sem dependência de runtime | +| `public/` | o **jogo** | 44 arquivos `.js`, 32.174 linhas · Three.js `r160` vendorizado | ES modules servidos crus, **zero build**, sem dependência de runtime | | `src/` | o **site** | 18 páginas `.astro`, 21 rotas `/api` · Astro `^7.1.1` | framework é bem-vindo; `service_role` só no servidor | | `tools/` | o **arnês** | 201 scripts em `tools/eval/`, 55 em `tools/` | node puro: sobe o jogo real sem browser | diff --git a/README.md b/README.md index cc453b6d..337c1fbe 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ contra bots, direto na aba. Sem download, sem instalação, sem cadastro. | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 32.001 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | -| `game.js` | **6.910** linhas | `wc -l public/js/game.js` | +| Código do jogo | 32.174 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **7.083** linhas | `wc -l public/js/game.js` | | `main.js` | 2.698 linhas | `wc -l public/js/main.js` | | Armas com GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | | GLBs de personagem | 45 | `git ls-files 'public/models/characters/*.glb' \| wc -l` | diff --git a/STATUS.md b/STATUS.md index 02b49e34..77066104 100644 --- a/STATUS.md +++ b/STATUS.md @@ -4,7 +4,7 @@ - **Versão:** `2.0.0-alpha.186` - **Conteúdo jogável:** 5 facções, 44 personagens, 12 mapas e 26 armas com GLB -- **Código do jogo:** 32.001 linhas em 44 módulos JavaScript +- **Código do jogo:** 32.174 linhas em 44 módulos JavaScript - **Automação:** 124 comandos npm, 201 scripts de avaliação e 55 scripts de pipeline > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: `package.json · CHARACTERS · MAPS · public/models/weapons · public/js · tools/` diff --git a/docs/RIG-PEGA-ARMA.md b/docs/RIG-PEGA-ARMA.md new file mode 100644 index 00000000..8fe0a121 --- /dev/null +++ b/docs/RIG-PEGA-ARMA.md @@ -0,0 +1,173 @@ +# Pega de arma no rig (Blender) — receita replicável + +> **Feito em:** 2026-08-23 · **Personagem-piloto:** `mandrake` · **Arma-piloto:** `pistol` +> **Objetivo:** montar, à mão no Blender, a pega correta de uma arma no rig do personagem +> (mão de lado envolvendo o cabo, cano pra frente), extrair os **números exatos** e +> deixar registrado pra replicar em outras armas e outros personagens. + +Este documento é a régua da tarefa: quem replicar num segundo par (arma/personagem) tem +que chegar nos mesmos passos e nos mesmos tipos de número. Se um passo não bater, é sinal +de que o rig do novo par diverge — e isso é informação, não erro. + +--- + +## 0. Contexto do rig (o que fez a coisa dar trabalho) + +- O corpo do personagem vem em `models/characters/.glb` (malha + textura + skin). + O **rig do jogo** tem 24 ossos (Hips/Spine/…/RightHand), **sem ossos de dedo**. +- O rig usado aqui pra AUTORAR a pega veio do `Flair.fbx` (Mixamo, 65 ossos **com dedos**: + `mixamorig:RightHandIndex1..3`, `Middle`, `Ring`, `Pinky`, `Thumb1..3`). A malha do + mandrake dentro do FBX é a **mesma** (6129 verts, UV idêntico) — por isso deu pra aplicar + o material texturizado nela e ver a pega com textura. +- **Escala:** o FBX Mixamo entra com `scale = 0.01` → o personagem fica com **1,7 cm**. + Normalizar `Armature.001.scale = 1.0` deixa ele em **1,7 m** (bate com as armas e com o + jogo). **Sempre normalize a escala antes de medir/posicionar qualquer coisa.** +- **Convenção de eixos** (personagem em pé): forward do personagem = **-Y**, up = **+Z**. + +### Gotcha crítico do viewport (custou tempo, anota) +Esconder o esqueleto pelo **`hide_viewport`** (ícone monitor) **desliga a avaliação da IK** +e **congela a deformação do mesh** numa pose antiga — parece que "perdeu a pega", mas os +dados estão certos. Regras: +1. Com rig que usa **IK**, **asse a IK em FK** antes de esconder (`pose.visual_transform_apply` + + remover as constraints de IK). +2. Pra esconder os ossos sem quebrar, use o **"olho" (`hide_set`)**, nunca o monitor. +3. Se o mesh congelar mesmo assim: **entrar e sair do Modo de Edição** do mesh força o + recálculo do modifier (destrava na hora). + +--- + +## 1. Passo a passo (o que foi feito, em ordem) + +1. **Importar** `models/characters/mandrake.glb` (corpo+textura) e o `Flair.fbx` (rig+anim). + Aplicar o material `Material_1` (texturas: `texture_0` albedo 1024, `mandrake_normal`, + `mandrake_metalrough`) na malha skinada do rig Mixamo (`char1.001`). UV é idêntico. +2. **Normalizar escala** do rig: `Armature.001.scale = (1,1,1)` → personagem 1,7 m. +3. **Pose do braço** (uma mão, pistola): alvos de **IK** (empties) nas mãos + constraint de + IK de 2 ossos em cada antebraço. + - Mão direita (do gatilho): alvo à frente, ~altura do ombro → `(-0.05, -0.42, 1.24)`. + - Mão esquerda (apoio, relaxada no quadril) → `(0.17, -0.05, 0.92)`. +4. **Roll da mão** direita: girar `mixamorig:RightHand` **+90° no eixo Y local** → a normal + da palma sai de "pra baixo" (-Z) pra "de lado, pra dentro" (+X). Sem isso a palma fica + pra baixo e a arma "deitada". +5. **Curl dos dedos** (formar o punho em volta do cabo) — rotação por falange no eixo X + local (polegar no eixo Z local). Ângulos usados (graus): + | dedo | seg1 | seg2 | seg3 | + |--------|------|------|------| + | Index | 38 | 45 | 35 | + | Middle | 62 | 70 | 55 | + | Ring | 64 | 72 | 58 | + | Pinky | 60 | 66 | 52 | + | Thumb | 28 | 34 | 26 | + > Index é o dedo do gatilho: fecha menos que os outros três. +6. **Assentar a arma** (o passo que "prende" a arma na mão): + - Medir na malha da pistola o **centro do cabo** (backstrap): vértices do terço traseiro + em X **e** metade inferior em Z → centroide `grip_hold`. + - Medir o **centro do punho**: centroide das bases dos dedos (`*1` das 4 fingers). + - Posicionar a arma de modo que `grip_hold` caia no centro do punho, cano em **-Y**, + topo do slide em **+Z**, escala pro tamanho real (`CFG.pistol.len = 0.26 m`). + - Ajuste fino pedido pelo dono: puxar a arma **+3,5 cm em Y** (pra trás, sentido oposto + ao cano) pra o cabo assentar mais fundo, encostando no "V" do polegar. +7. **Assar IK→FK** e salvar a pose como Action **`pistol_aim_R`**. + +--- + +## 2. Números extraídos (a saída reutilizável) + +> Estes são os números que replicam a pega. O **mount** é o mais importante: é o transform +> da arma no espaço LOCAL do osso da mão — mesmo formato do mount procedural do jogo +> (`glbchars.js`, `handBone.add(mount)`). + +### Mount da PISTOLA (local a `mixamorig:RightHand`) +``` +loc (m) = (0.0389, 0.1186, 0.0095) +rot (deg) = (-84.85, -8.58, -98.66) # Euler XYZ +scale = 0.2098 +``` + +### Roll da mão direita +``` +mixamorig:RightHand.rotation_quaternion = (0.7071, 0, 0.7071, 0) # 90° em Y local +``` + +### Curl dos dedos (graus) — ver tabela no passo 5. + +### Alvos de IK (mundo, personagem 1,72 m, forward -Y) +``` +RightHand target = (-0.05, -0.42, 1.24) +LeftHand target = ( 0.17, -0.05, 0.92) # apoio no quadril (pose de 1 mão) +``` + +--- + +## 3. Como replicar + +### Outra ARMA, mesmo personagem +1. Importar o GLB da arma (`models/weapons/.glb`), escalar pra `CFG..len` (weapons.js). +2. **Rifle (2 mãos):** manter o mount da mão direita no cabo e **adicionar a mão esquerda** + no guarda-mão (ponto `gripPoints(id).fore` — já existe no jogo). No Blender, um 2º alvo + de IK na mão esquerda, no guarda-mão. +3. **Pistola/revólver (1 mão):** reusar direto os números da seção 2; só re-medir o + `grip_hold` na malha da arma nova (o cabo muda de lugar). +4. Re-medir só o que muda: `grip_hold` (centro do cabo na malha) e a escala (`CFG.len`). + Roll da mão e curl dos dedos **não mudam** por arma do mesmo tipo. + +### Outro PERSONAGEM +- Se o outro personagem usa **o mesmo rig Mixamo** (mesmos nomes de osso), os números da + seção 2 valem **iguais** — mão e dedos são função do rig, não do corpo. +- Se o rig diverge (proporção de braço/mão diferente), o **mount** e o **curl** valem, mas + os **alvos de IK** mudam (a mão nasce em outro lugar). Re-rodar o passo 3. +- Personagens de "braço-toco" (Dollynho, ET, Canarinho, Gotinha): a palma nasce dentro do + corpo — ver o tratamento que o jogo já faz (`torsoProfile`/`measurePalmLocal` em + `glbchars.js`) antes de assumir que a pega "bugou". + +--- + +## 4. Ligação com o sistema do jogo (não reinventar) + +O jogo **já resolve arma-na-mão** proceduralmente; esta pega manual é a versão "à mão" do +mesmo conceito. Pontos de conexão: + +- `weapons.js → gripPoints(id)`: `grip` (mão direita) e `fore` (mão esquerda no guarda-mão, + `null` p/ 1 mão). `ONE_HANDED = {pistol, deagle, revolver38, knife}`. +- `glbchars.js → buildCharacterModel`: monta a arma no `RightHand`, IK da mão de apoio + (`solveCCDIK`), curl dos dedos. **É o caminho da 3ª pessoa (bots).** +- `fparms.js → poseToWeapon`: braços dedicados de 1ª pessoa (`models/fparms/arms.glb`, + **sem ossos de dedo**), travados na arma por IK a cada frame. + +> **Implicação p/ 1ª pessoa:** o rig de braço FP (`arms.glb`) é diferente do Mixamo (não tem +> dedos). O que transfere direto é o **mount** (arma↔osso da mão) e a **orientação da pega**; +> o curl dos dedos não (dedos já vêm fechados na malha do `arms.glb`). + +--- + +## 5. Câmera 3ª pessoa no jogo (implementado) + +**Tecla `B`** alterna 1ª/3ª pessoa. Arquivo: `public/js/game.js`. + +Como funciona (reusa o que já existia): +- O corpo 3ª pessoa do jogador é o **mesmo `buildCharacterModel` dos bots** (`glbchars.js`) + — a arma monta na mão pelo grip procedural (mount no `RightHand` + IK da mão de apoio + + curl dos dedos). Nada de pose nova: é o mesmo caminho que já funciona pros bots. +- Métodos: `_toggleCamView` / `_ensurePlayerTP` (lazy build + upgrade box→GLB + rebuild ao + trocar de arma) / `_updatePlayerTP` (posição+yaw do jogador, locomoção pela velocidade, + crouch, mira da cabeça pelo pitch, câmera atrás). +- Visibilidade: em 3ª pessoa esconde `vm.root` (braços+arma FP) e mostra o corpo TP. +- Pré-carrega o modelo do personagem do jogador no construtor (a 1ª pessoa usa `arms.glb`, + não carrega o corpo), senão o TP cai no box. + +**Limitação declarada do V1:** mira/tiro continuam saindo da CÂMERA → em 3ª pessoa a origem +do disparo fica atrás do jogador. É um **modo de visão**, não de combate. Corrigir depois: +desacoplar a origem do tiro (olho) da câmera de render. + +**Como testar:** entrar numa partida, apertar **B**. Esperado: câmera vai pra trás e aparece +o mandrake segurando a pistola (o mesmo grip dos bots). Apertar B de novo volta pra 1ª pessoa. +Se o corpo aparecer virado de costas, é só o offset de yaw — trocar `p.yaw` por `p.yaw+Math.PI` +em `_updatePlayerTP`. + +## 6. Próximos + +- [x] Pega da pistola no mandrake (3ª pessoa, à mão) — números na seção 2, Action `pistol_aim_R`. +- [x] Toggle de **câmera 3ª pessoa** pro jogador (tecla B, reusa `buildCharacterModel`). +- [ ] Aplicar o mount à pistola na **1ª pessoa** (tunar `fparms.js`: offset/rot por arma). +- [ ] Tiro correto em 3ª pessoa (origem no olho, não na câmera). +- [ ] Replicar p/ rifle (2 mãos) e demais personagens. diff --git a/docs/docs/arquitetura.md b/docs/docs/arquitetura.md index be541632..60aca2be 100644 --- a/docs/docs/arquitetura.md +++ b/docs/docs/arquitetura.md @@ -62,7 +62,7 @@ Tamanho dos arquivos que o `gen-arch.mjs` indexa — bloco gerado, regenerado po | Arquivo | Linhas | |---|---:| -| `public/js/game.js` | 6.910 | +| `public/js/game.js` | 7.083 | | `public/js/main.js` | 2.698 | | `public/js/characters.js` | 1.068 | | `public/js/glbchars.js` | 844 | @@ -70,7 +70,7 @@ Tamanho dos arquivos que o `gen-arch.mjs` indexa — bloco gerado, regenerado po | `public/js/weapons.js` | 346 | | `public/js/springs.js` | 260 | -Total de `public/js/`: **32.001 linhas em 44 arquivos**. O índice símbolo→linha, com a tabela de conflito, é outro bloco gerado: `tools/eval/ARCH.md` (`npm run arch`). +Total de `public/js/`: **32.174 linhas em 44 arquivos**. O índice símbolo→linha, com a tabela de conflito, é outro bloco gerado: `tools/eval/ARCH.md` (`npm run arch`). > Bloco gerado por `node tools/gen-docs.mjs`. Fonte: ``git ls-files public/js/*.js | xargs wc -l`` diff --git a/docs/docs/comecando.md b/docs/docs/comecando.md index fca2929b..d45160d2 100644 --- a/docs/docs/comecando.md +++ b/docs/docs/comecando.md @@ -39,8 +39,8 @@ esta página envelhecia no primeiro commit — ver | O que | Quanto | Onde confere | |---|---:|---| -| Código do jogo | 32.001 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | -| `game.js` | **6.910** linhas | `wc -l public/js/game.js` | +| Código do jogo | 32.174 linhas em 44 arquivos | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **7.083** linhas | `wc -l public/js/game.js` | | `main.js` | 2.698 linhas | `wc -l public/js/main.js` | | Armas com GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | | GLBs de personagem | 45 | `git ls-files 'public/models/characters/*.glb' \| wc -l` | diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md index bf263a22..383df90d 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/arquitetura.md @@ -69,7 +69,7 @@ Size of the files `gen-arch.mjs` indexes — a generated block, regenerated by | File | Lines | |---|---:| -| `public/js/game.js` | 6,910 | +| `public/js/game.js` | 7,083 | | `public/js/main.js` | 2,698 | | `public/js/characters.js` | 1,068 | | `public/js/glbchars.js` | 844 | @@ -77,7 +77,7 @@ Size of the files `gen-arch.mjs` indexes — a generated block, regenerated by | `public/js/weapons.js` | 346 | | `public/js/springs.js` | 260 | -Total in `public/js/`: **32,001 lines in 44 files**. The symbol-to-line index lives in `tools/eval/ARCH.md`. +Total in `public/js/`: **32,174 lines in 44 files**. The symbol-to-line index lives in `tools/eval/ARCH.md`. > Block generated by `node tools/gen-docs.mjs`. Source: ``wc -l public/js/*.js`` diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md index 71fc2e8c..d8501fb8 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/comecando.md @@ -41,8 +41,8 @@ this page was aging at the very first commit — see | What | How much | Where to check | |---|---:|---| -| Game code | 32,001 lines in 44 files | `git ls-files public/js/*.js \| xargs wc -l` | -| `game.js` | **6,910** lines | `wc -l public/js/game.js` | +| Game code | 32,174 lines in 44 files | `git ls-files public/js/*.js \| xargs wc -l` | +| `game.js` | **7,083** lines | `wc -l public/js/game.js` | | `main.js` | 2,698 lines | `wc -l public/js/main.js` | | Weapons with GLB | 26 | `git ls-files 'public/models/weapons/*.glb' \| wc -l` | | Character GLBs | 45 | `git ls-files 'public/models/characters/*.glb' \| wc -l` | diff --git a/public/js/game.js b/public/js/game.js index c9d32bee..4519af89 100644 --- a/public/js/game.js +++ b/public/js/game.js @@ -2,7 +2,7 @@ import * as THREE from 'three'; import { MAPS, resolveMapId } from './maps.js'; import { buildCharacter, poseCharacter, byId, CHARACTERS, buildRifle, charWeapon } from './characters.js'; -import { buildCharacterModel, hasModel } from './glbchars.js'; +import { buildCharacterModel, hasModel, preloadCharacterAssets } from './glbchars.js'; import { weaponModel, weaponCFG, ONE_HANDED, WEAPON_IDS, PISTOLS, gripPoints } from './weapons.js'; import { buildFPArms, poseToWeapon, FP_OFF } from './fparms.js'; import { VM_FRAME } from './vmattach.js'; @@ -679,6 +679,25 @@ export class Game { this.playerCharId = playerCharId; // usado por _buildViewModels (paleta/braços FP) e _resetPositions (loadout) this.combatants = []; // scoreboard entries + // ---- Câmera 3ª pessoa (toggle KeyB) ---------------------------------------- + // O jogo é 1ª pessoa; os bots já são 3ª pessoa (glbchars.js). Aqui o JOGADOR ganha + // um corpo 3ª pessoa (mesmo buildCharacterModel dos bots — a arma monta na mão pelo + // MESMO grip procedural: mount no RightHand + IK da mão de apoio + curl dos dedos) e + // uma câmera atrás dele. Tecla B alterna. Ver docs/RIG-PEGA-ARMA.md. + // V1 DECLARADO: mira/tiro continuam saindo da câmera — em 3ª pessoa a origem do + // disparo fica atrás do jogador. É um MODO DE VISÃO, não de combate. + // Pré-carrega o modelo do próprio personagem (a 1ª pessoa usa arms.glb, não carrega o + // corpo do jogador), senão o corpo TP cai no box procedural. + this.camView = 'first'; // 'first' (1ª pessoa) | 'third' (OTS SOCOM) | 'shoulder' (OTS colado) + this.playerTP = null; + this._tpWeapon = null; + this._tpDead = false; + this._tpFwd = new THREE.Vector3(); + this._tpRight = new THREE.Vector3(); + this._tpEul = new THREE.Euler(); + this._eyeWorld = new THREE.Vector3(); // posição do OLHO — origem de tiro/fumaça em 3ª pessoa + try { preloadCharacterAssets([playerCharId]); } catch {} + // ---- player ---- // Spawns holding the SAME weapon shown on the character-select screen (charWeapon). // primary/secondary remember the last weapon of each slot for the 1/2 keys. @@ -960,7 +979,7 @@ export class Game { this._vmFlashLight.position.set(0.1, -0.06, -0.75); // boca do cano em view space (pose GAUNTLET 2.0) this.vmScene.add(this._vmFlashLight); this._vmFlash = { t: 1, life: 0.045, peak: 1.6 }; - this._fxTune = { light: 1, flash: 1, spark: 1 }; // multiplicadores de FX (dev.html game-backed) + this._fxTune = { light: 1, flash: 1, spark: 1, smoke: 1 }; // multiplicadores de FX (dev.html game-backed) } this.scene.userData.vmPass = { scene: this.vmScene, camera: this.vmCamera }; @@ -1003,6 +1022,16 @@ export class Game { // share another (soft smoke) — 1 draw call each, zero per-shot allocation (ring buffer). this.flashFx = new GPUParticles(this.scene, this.camera, { tex: this.flashTex, additive: true }); this.puffFx = new GPUParticles(this.scene, this.camera, { tex: this.puffTex, additive: false }); + // fumaça do cano: nuvem cinza persistente por tiro. _tintFx compartilha uTime/uScale do puff + // (anima sem tick novo) e em quality 'low' devolve o próprio puffFx (degrada sozinho). + // sistema CINZA dedicado — NÃO cai no puffFx bege em quality:'low' (o dono joga em máquina + // fraca, é justamente onde ele quer ver a fumaça). 1 draw call, ring buffer de 64, compartilha + // uTime/uScale do puff (anima sem tick próprio, igual às fx de sangue via _tintFx). + this._muzzleSmokeFx = new GPUParticles(this.scene, this.camera, { tex: this._makeSmokeTex(), additive: false, max: 64 }); + if (this.puffFx && this.puffFx.uniforms) { + this._muzzleSmokeFx.uniforms.uTime = this.puffFx.uniforms.uTime; + this._muzzleSmokeFx.uniforms.uScale = this.puffFx.uniforms.uScale; + } // Muzzle flash (R7.5): 2 SPRITES additivos por tiro — estrela irregular com ruído + // núcleo branco-quente — compactos (0.35-0.5m), na boca do VM (baixo-direita), vida // ≤3 frames (~50ms). Era um cone de 8 segmentos + icosaedro escalado até 1.4 spawnado @@ -1070,8 +1099,8 @@ export class Game { // arma→fallback no _muzzleWorld. Object.assign(this._vmMuzzle, this._vmMuzzleExt || {}); // cápsulas (brass) ejetadas a cada tiro — geo/mat compartilhados, pool reusado - this._casingGeo = new THREE.CylinderGeometry(0.011, 0.011, 0.034, 6); - this._casingMat = new THREE.MeshStandardMaterial({ color: 0xd9a441, metalness: 0.85, roughness: 0.4 }); + this._casingGeo = new THREE.CylinderGeometry(0.016, 0.016, 0.05, 8); + this._casingMat = new THREE.MeshStandardMaterial({ color: 0xd9a441, metalness: 1.0, roughness: 0.35, emissive: 0x3a2a10, emissiveIntensity: 0.4 }); this._casings = []; this._casingPool = []; // granada de fumaça: projétil (mesh) + nuvem de sprites billboard que bloqueia a visão dos bots this._grenades = []; this._smokes = []; @@ -1592,6 +1621,8 @@ export class Game { } } // SÓ-ARMA: esconde as mãos procedurais (handR/handL) presas a cada modelo de arma. + // (A handR procedural é cápsulas calibradas pra arma-box antiga — na arma Mint cai + // torta. Mão boa vem do corpo real do personagem: ver os modos de 3ª pessoa (camView).) if (WEAPON_ONLY) for (const k in models) models[k].traverse((o) => { if (o.name === 'handR' || o.name === 'handL') o.visible = false; }); this._weaponOnly = WEAPON_ONLY; // grip/ads expostos no objeto do VM (G3-R1): `grip[id]` é o PONTO DE EMPUNHADURA em @@ -1801,6 +1832,7 @@ export class Game { if (e.code === 'KeyZ') { this._radioShow('z'); return; } if (e.code === 'KeyX') { this._radioShow('x'); return; } if (e.code === 'KeyV') { this._radioShow('c'); return; } + if (e.code === 'KeyB') { this._toggleCamView(); return; } // 1ª/3ª pessoa on/off // slot memory: 1 = last primary held, 2 = last sidearm held (not a hardcoded reset) if (e.code === 'Digit1') this._switchWeapon(this.player.primary || 'awp'); if (e.code === 'Digit2') this._switchWeapon(this.player.secondary || 'pistol'); @@ -2942,7 +2974,7 @@ export class Game { p.pitch = Math.max(-1.45, Math.min(1.45, p.pitch + vy * REC.perm)); // mesmo clamp do mouse-look p.yaw -= hx * REC.perm; st.last = this.time; - st.sh = Math.min(0.013, st.sh + g * 0.16); + st.sh = Math.min(0.020, st.sh + g * 0.22); } _tryShoot() { const p = this.player, w = WEAPONS[p.weapon]; @@ -2981,7 +3013,7 @@ export class Game { const spreadBase = (GUNFEEL ? (w.spreadHip + (spScoped - w.spreadHip) * adsF) : (p.weapon === 'awp' ? (p.scoped ? w.spreadScope : w.spreadHip) : w.spreadHip)) * crouchMul * moveMul; - const from = this.camera.getWorldPosition(new THREE.Vector3()); + const from = this._aimOrigin(new THREE.Vector3()); const pellets = w.pellets || 1; // tracer só em PARTE dos tiros (CS): 1 em 3 na rajada; sniper/shotgun sempre (o tiro é o // evento). Antes TODO tiro deixava rastro — vira "chuva de laser" em full-auto. @@ -3028,7 +3060,7 @@ export class Game { if (p.scoped && (p.weapon === 'awp' || p.weapon === 'mosin' || p.weapon === 'rem700')) this._scope(false, true); } _meleeHit() { - const from = this.camera.getWorldPosition(new THREE.Vector3()); + const from = this._aimOrigin(new THREE.Vector3()); const dir = new THREE.Vector3(0, 0, -1).applyQuaternion(this.camera.quaternion); let best = null, bd = WEAPONS.knife.range; for (const b of this.bots) { @@ -3647,17 +3679,44 @@ export class Game { this.flashFx.spawn(pos, { vel: v, life: 0.06 + Math.random() * 0.05, size: fpCls ? 0.07 : 0.11, grow: -0.4 }); } this.puffFx.spawn(pos.clone().addScaledVector(d, 0.18), { vel: d.clone().multiplyScalar(1.2), life: 0.3, size: fpCls ? 0.16 : 0.28, grow: 0.9 }); + // fumaça do cano: 2-3 baforadas lentas subindo/à frente, vida longa, crescendo. Menor e mais + // perto na 1ª pessoa (fpCls) pra não virar blob colado na lente (mesmo cuidado das faíscas, R7.6). + const smokeN = fpCls ? 3 : 4; + const smokeSize = fpCls ? 0.2 : 0.32; + const smU = new THREE.Vector3(0, 1, 0); + for (let i = 0; i < Math.round(smokeN * ((this._fxTune && this._fxTune.smoke) ?? 1)); i++) { + const sv = d.clone().multiplyScalar(0.6 + Math.random() * 0.5) + .addScaledVector(smU, 0.5 + Math.random() * 0.4) + .add(new THREE.Vector3((Math.random() - 0.5) * 0.5, 0, (Math.random() - 0.5) * 0.5)); + this._muzzleSmokeFx.spawn(pos.clone().addScaledVector(d, 0.10 + Math.random() * 0.1), + { vel: sv, life: 0.45 + Math.random() * 0.35, size: smokeSize, grow: 1.6 }); + } } // Boca do cano em WORLD SPACE no instante do tiro: offset local da classe transformada // pelo matrixWorld ATUAL do vm.root (com o kick acumulado) e depois pela câmera — usado // pelo tracer e pela luz/faísca do mundo no tiro do jogador (R7.6). _muzzleWorld(cls) { + if (this.camView !== 'first') { + // 3ª pessoa: a câmera está atrás/acima; a boca do cano NÃO pode sair dela (fumaça + // nascia lá em cima — bug relatado). Sai do OLHO, na direção da mira, descida pra + // altura da arma na mão. + const dir = this.camera.getWorldDirection(new THREE.Vector3()); + const m = this._eyeWorld.clone().addScaledVector(dir, 0.5); + m.y -= 0.28; + return m; + } const off = this._vmMuzzle[this.player?.weapon] || this._vmMuzzle[cls] || this._vmMuzzle.rifle; this.vm.root.updateWorldMatrix(true, false); const v = off.clone(); this.vm.root.localToWorld(v); // vmScene == espaço da câmera (vmCamera na origem) return this.camera.localToWorld(v); } + + // Origem do tiro/raycast: olho do jogador. Em 3ª pessoa a câmera está atrás — usar a + // câmera faria a bala nascer nas costas. Ver _eyeWorld (capturado no _updatePlayer). + _aimOrigin(out) { + return this.camView === 'first' ? this.camera.getWorldPosition(out) : out.copy(this._eyeWorld); + } // Porta com SENSOR (Havan): desliza as 2 folhas ao chegar perto (player ou bot). Painéis são // só visuais (não colidem), então quando você alcança a porta já está aberta. _updateDoors(dt) { @@ -3734,10 +3793,10 @@ export class Game { const right = new THREE.Vector3(1, 0, 0).applyQuaternion(q); const up = new THREE.Vector3(0, 1, 0).applyQuaternion(q); const back = new THREE.Vector3(0, 0, 1).applyQuaternion(q); - c.v.copy(right).multiplyScalar(2.2 + Math.random() * 0.9).addScaledVector(up, 1.7 + Math.random() * 0.6).addScaledVector(back, 0.5 + Math.random() * 0.4); + c.v.copy(right).multiplyScalar(2.2 + Math.random() * 0.9).addScaledVector(up, 2.0 + Math.random() * 0.6).addScaledVector(back, 0.5 + Math.random() * 0.4); c.av.set((Math.random() - 0.5) * 24, (Math.random() - 0.5) * 24, (Math.random() - 0.5) * 24); c.m.rotation.set(Math.random() * 6, Math.random() * 6, Math.random() * 6); - c.m.scale.setScalar(1); c.ttl = 1.6; + c.m.scale.setScalar(1); c.ttl = 2.2; this.scene.add(c.m); this._casings.push(c); } @@ -4794,6 +4853,105 @@ export class Game { if (this.vm?.root) this.vm.root.visible = false; if (this.el.crosshair) this.el.crosshair.style.display = 'none'; } + // Cicla 1ª → 3ª (longe) → ombro (perto, vê braços) → 1ª (tecla B). Constrói o corpo + // 3ª pessoa na 1ª vez (lazy). + _toggleCamView() { + // first = 1ª pessoa (só-arma, padrão do jogo) · third = OTS SOCOM (cintura pra cima) · + // shoulder = OTS colado (vê a arma de perto). A 1ª-pessoa-com-corpo (fpbody) foi removida. + const order = ['first', 'third', 'shoulder']; + this.camView = order[(order.indexOf(this.camView) + 1) % order.length]; + if (this.camView !== 'first') this._ensurePlayerTP(); + this._syncCamViewVis(); + } + + // Visibilidade base ao alternar: corpo TP em qualquer modo 3ª pessoa. (Os braços FP/ + // vm.root são governados por frame no _updatePlayer — ver a linha do realScope — pra não + // brigar com o force-visible do ADS/luneta.) + _syncCamViewVis() { + const tp = this.camView !== 'first'; + if (this.playerTP && this.playerTP.group) this.playerTP.group.visible = tp; + if (this.vm && this.vm.root && !tp) this.vm.root.visible = true; + } + + // (Re)constrói o corpo 3ª pessoa do jogador com a arma ATUAL montada na mão. Reusa + // buildCharacterModel (mesmo caminho dos bots: grip + IK + curl). Se o modelo do + // personagem ainda não carregou, cai no buildCharacter (box) e faz upgrade p/ GLB + // quando o asset chegar (hasModel). Reconstrói ao trocar de arma. + _ensurePlayerTP() { + const w = this.player.weapon, def = this.playerDef; + const weaponChanged = this.playerTP && this._tpWeapon !== w; + const wantUpgrade = this.playerTP && !this.playerTP.isGLB && hasModel(def.id); + if (this.playerTP && !weaponChanged && !wantUpgrade) return; + // Tenta o GLB (grip real). Se ainda não carregou e já temos algo, mantém o atual — + // evita reconstruir o box a cada frame enquanto os clips não chegam. + const glb = buildCharacterModel(def, { weaponId: w }); + if (!glb && this.playerTP && !weaponChanged) return; + if (this.playerTP && this.playerTP.group) { + this.scene.remove(this.playerTP.group); + if (!this.playerTP.isGLB) this.playerTP.group.traverse(o => { if (o.geometry) o.geometry.dispose(); }); + } + const tp = glb || buildCharacter(def); + // O corpo do próprio jogador nunca é alvo/obstáculo de raycast (o hitscan mira só os + // grupos dos bots, mas marcamos por garantia — mesmo contrato do gun mount). + tp.group.traverse(o => { o.userData.noHit = true; o.userData.selfBody = true; }); + tp.group.visible = (this.camView === 'third'); + this.scene.add(tp.group); + this.playerTP = tp; + this._tpWeapon = w; + } + + // Anima o corpo TP a partir do estado do jogador e põe a câmera atrás dele. + // `sp` = velocidade horizontal real; `eye` = altura do olho já com crouch/dip. + _updatePlayerTP(dt, sp, eye) { + this._ensurePlayerTP(); + const tp = this.playerTP, p = this.player; + if (!tp) return; + // corpo: posição + yaw do jogador (mesma convenção dos bots: rotation.y = yaw). + tp.group.visible = true; + tp.group.position.copy(p.pos); + tp.group.rotation.set(0, p.yaw + Math.PI, 0); // +180°: o corpo olha pra onde a câmera olha (de costas p/ ela) + // animação: locomoção pela velocidade + agachado + mira da cabeça pelo pitch. + if (tp.ctrl) { + tp.ctrl.setCrouch?.(p.crouchF > 0.5); + tp.ctrl.aimPitch = p.pitch; + tp.ctrl.update(dt, sp > 0.6 ? 1 : 0, false, sp, false); + } else if (tp.mixer) { + tp.mixer.update(dt); + } + this._tpEul.set(p.pitch, p.yaw, 0, 'YXZ'); + const fwd = this._tpFwd.set(0, 0, -1).applyEuler(this._tpEul); + const right = this._tpRight.set(1, 0, 0).applyEuler(this._tpEul); + const cam = this.camera; + // 'third' = over-the-shoulder perto, cintura pra cima (estilo SOCOM): ~1,7 m atrás, + // ~1,8 m de altura olhando reto → pernas caem fora. 'shoulder' = mais colado ainda. + const shoulder = this.camView === 'shoulder'; + const TP_DIST = shoulder ? 0.85 : 1.7; + const TP_UP = shoulder ? 0.10 : 0.18; + const TP_SIDE = shoulder ? 0.34 : 0.28; + cam.position.set(p.pos.x, p.pos.y + eye, p.pos.z).addScaledVector(fwd, -TP_DIST).addScaledVector(right, TP_SIDE); + cam.position.y += TP_UP; + const gy = this.world.groundHeightAt(cam.position.x, cam.position.z, cam.position.y) + 0.2; + if (cam.position.y < gy) cam.position.y = gy; // não atravessa o chão + cam.rotation.set(p.pitch, p.yaw, 0); + } + + // Morte em 3ª pessoa (e também visível na 1ª, na câmera de morte): toca a animação de + // queda no corpo TP e o mantém caindo. Sem isto o boneco ficava de pé (bug relatado). + _tpDeath(dt) { + this._ensurePlayerTP(); + const tp = this.playerTP, p = this.player; + if (!tp) return; + if (!this._tpDead) { + this._tpDead = true; + tp.group.visible = true; // mostra o corpo caindo mesmo em 1ª pessoa + tp.group.position.copy(p.pos); + tp.group.rotation.set(0, p.yaw + Math.PI, 0); + if (tp.ctrl && tp.ctrl.die) tp.ctrl.die(); + } + if (tp.ctrl) tp.ctrl.update(dt, 0, false, 0, false); + else if (tp.mixer) tp.mixer.update(dt); + } + _updatePlayer(dt) { const p = this.player; this._checkCtfAlvo(); // alvo de BANDEIRAS: única condição de vitória da rodada de CAPTURA (sem gate) @@ -4803,11 +4961,18 @@ export class Game { const left = p.respawnAt - this.time; this.el.respawnCount.textContent = Math.max(0, left).toFixed(1); this._deathFeedback(dt); + this._tpDeath(dt); // corpo TP cai (item: "boneco não caía morto") if (left <= 0) this._respawnPlayer(); this.camera.position.y = Math.max(0.5, this.camera.position.y - dt * 2); this.camera.rotation.z = Math.min(0.5, (this.camera.rotation.z || 0) + dt * 0.8); return; } + // ressuscitou: desfaz a morte do corpo TP e ajusta visibilidade ao modo atual. + if (this._tpDead) { + this._tpDead = false; + if (this.playerTP && this.playerTP.ctrl && this.playerTP.ctrl.revive) this.playerTP.ctrl.revive(); + if (this.playerTP && this.playerTP.group) this.playerTP.group.visible = this.camView !== 'first'; + } if (this.mobile && this.state === 'live') this._aimAssist(dt); // sticky aim (a mira é por arraste) // REGEN fora de combate (ver comentário da constante). Detecta o dano pela QUEDA do hp — // o _damage fica fora desta região de edição, então não dá pra marcar o timestamp lá. @@ -4927,7 +5092,14 @@ export class Game { camBobLat = Math.sin(p.stepPhase) * 0.009 * amp; } this.camera.position.set(p.pos.x + Math.cos(p.yaw) * camBobLat, p.pos.y + eye + camBobY, p.pos.z - Math.sin(p.yaw) * camBobLat); - this.camera.rotation.set(p.pitch + p.recoilP, p.yaw, 0); + const rp = p.recoilP; // lê o getter 1x (integra/decai o spring do recuo neste frame) + // roll leve por tiro: reusa o mesmo st.sh decadente numa freq. diferente do pitch (61 vs 78) + // → lê como "tremida" e não como punch puro. Vale pra mouse E toque (câmera finaliza só aqui). + const roll = p._rec ? p._rec.sh * Math.sin(this.time * 61) * 0.5 : 0; + this.camera.rotation.set(p.pitch + rp, p.yaw, roll); + this._eyeWorld.copy(this.camera.position); // olho: origem de tiro/fumaça em 3ª pessoa + // 3ª pessoa (longe/ombro): sobrepõe a câmera e anima o corpo TP. No-op na 1ª pessoa. + if (this.camView !== 'first') this._updatePlayerTP(dt, sp, eye); // footsteps + view bob const moving = sp > 0.6 && p.grounded; if (moving) { @@ -4980,7 +5152,8 @@ export class Game { // dynamic crosshair gap (movement/spray opens it, crouch + ADS tighten it) const gap = precAds ? 3 : Math.max(3, Math.min(26, 5 + sp * 1.15 + this.vm.kick * 20 - p.crouchF * 2.5 - (p.scoped ? 4 : 0))); this.el.crosshair.style.setProperty('--ch', gap.toFixed(1) + 'px'); - this.vm.root.visible = !(realScope && mask > 0.55); // a arma só sai de cena depois que a luneta cobre + // 3ª pessoa esconde os braços/arma FP (o corpo TP tem a própria arma na mão). + this.vm.root.visible = this.camView === 'first' && !(realScope && mask > 0.55); // a arma só sai de cena depois que a luneta cobre // reload completion — RELÓGIO DE JOGO (devolve a munição). A ANIMAÇÃO é do rig e usa a // mesma duração da tabela, então as duas pontas chegam no mesmo quadro (BUG-04). if (!this._reloading() && p.reloadUntil > 0) {