From 962bce1f257cd8ab73d6886123bdb0207cff50ec Mon Sep 17 00:00:00 2001 From: rubenmarcus Date: Tue, 25 Aug 2026 03:41:04 +0100 Subject: [PATCH 1/2] feat(campomorro): ergue torcida instanciada Agent: Codex Signed-off-by: rubenmarcus --- public/js/game.js | 1 + public/js/map_campomorro.js | 46 +++++++++++++++++++++++++++-- public/js/maps.js | 4 +-- tools/eval/campo-contract-check.mjs | 18 ++++++++++- 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/public/js/game.js b/public/js/game.js index b43192de..96b751c4 100644 --- a/public/js/game.js +++ b/public/js/game.js @@ -4449,6 +4449,7 @@ export class Game { _ctfWin(team) { this.roundsWon[team] = (this.roundsWon[team] || 0) + 1; + this.world.onGoal?.(); this.state = 'roundEnd'; this.stateUntil = this.time + 4; this.player.scoped = false; this.el.scope.classList.remove('on'); this.radioOpen = null; this._radioUi(); diff --git a/public/js/map_campomorro.js b/public/js/map_campomorro.js index 88172c24..6461959e 100644 --- a/public/js/map_campomorro.js +++ b/public/js/map_campomorro.js @@ -8,7 +8,7 @@ import { detailFor } from './textures.js'; import { applyLook } from './map_sky.js'; import { aplicaVento, updateVento } from './wind.js'; import { GPUParticles } from './gpuparticles.js'; -import { createFavelaAmbience } from './ambientlife.js'; +import { createFavelaAmbience, FAVELA_AMBIENCE_ASSETS } from './ambientlife.js'; import { AMB_LOOPS } from './soundscape.js'; const QP = new URLSearchParams(typeof location !== 'undefined' ? location.search : ''); @@ -47,6 +47,7 @@ export const CAMPOMORRO_PROPS = [ // RC4: grama da frente E (e-models) — o piloto do vento mora nela 'grama_corrego_01', 'grama_corrego_02', 'planta_corrego_taboa', 'planta_corrego_taioba', ]; +export const CAMPOMORRO_AMBIENCE = FAVELA_AMBIENCE_ASSETS; function laneHeight(x, z) { for (const l of LANES) { @@ -330,6 +331,46 @@ export function buildCampoMorro(scene, T = {}) { addBox(11.8, 0.55, 3.3, MAT.concrete, -7, gArq, 20, { collide: false, bala: true }); for (let i = 0; i < 3; i++) addBox(11.4 - i * 0.5, 0.34 + i * 0.28, 0.18, MAT.concrete, -7, groundHeightAt(-7, 18.36 + i * 0.08), 18.36 + i * 0.08, { collide: false, bala: true }); + // Cobertura e bancos fazem a arquibancada ler como estrutura de campo, não um bloco solto. + for (const x of [-12, -7, -2]) { + addBox(.18, 3.7, .18, MAT.steelRust, x, gArq + .2, 21.4, { collide: false, cast: false }); + addBox(.18, 3.7, .18, MAT.steelRust, x, gArq + .2, 18.3, { collide: false, cast: false }); + } + const cobertura = addBox(12.8, .18, 3.8, MAT.galpaoRoof, -7, gArq + 3.8, 19.85, { collide: false, cast: false }); + cobertura.userData.fieldStandRoof = true; + for (let fila = 0; fila < 3; fila++) { + const banco = addBox(11.1 - fila * .45, .16, .42, MAT.steelRust, -7, gArq + .68 + fila * .33, 19.0 + fila * .64, + { collide: false, cast: false }); + banco.userData.fieldStandBench = fila; + } + /* Torcida sem indivíduo identificável: uma silhueta compartilhada, 24 matrizes e uma chamada. + `onGoal` vem do ponto CTF; fora de gol a torcida senta, no gol ergue corpo e braços. */ + const corpoTorcida = mergeParts([ + new THREE.BoxGeometry(.34, .66, .22).translate(0, .38, 0), + new THREE.SphereGeometry(.16, 7, 6).translate(0, .86, 0), + new THREE.BoxGeometry(.62, .075, .075).translate(0, .58, 0), + ]); + const torcida = new THREE.InstancedMesh(corpoTorcida, new THREE.MeshStandardMaterial({ color: 0x303438, roughness: 1 }), 24); + torcida.userData.crowdInstanced = true; torcida.userData.crowdCount = 24; torcida.castShadow = true; torcida.receiveShadow = true; + root.add(torcida); + const torcidaSlots = []; + for (let fila = 0; fila < 3; fila++) for (let coluna = 0; coluna < 8; coluna++) + torcidaSlots.push({ x: -11.35 + coluna * 1.24 + (fila % 2) * .18, y: gArq + .6 + fila * .34, z: 19.0 + fila * .64, phase: fila * 8 + coluna }); + const dummyTorcida = new THREE.Object3D(); + let tempoTorcida = 0, golAte = 0; + const atualizarTorcida = (time) => { + const gol = time < golAte; + for (let i = 0; i < torcidaSlots.length; i++) { + const slot = torcidaSlots[i], balanco = Math.sin(time * 2.4 + slot.phase) * .035; + dummyTorcida.position.set(slot.x, slot.y + balanco + (gol ? .46 + (slot.phase % 3) * .06 : 0), slot.z); + dummyTorcida.rotation.set(gol ? Math.sin(time * 9 + slot.phase) * .18 : 0, Math.PI, 0); + dummyTorcida.scale.set(1, gol ? 1.18 : .72, 1); + dummyTorcida.updateMatrix(); torcida.setMatrixAt(i, dummyTorcida.matrix); + } + torcida.instanceMatrix.needsUpdate = true; + torcida.userData.crowdState = gol ? 'goal-standing' : 'seated'; + }; + atualizarTorcida(0); // Fachadas do beco oeste quebram a visada antes da entrada do campo. const CASAS = [[-32, 2], [-32, 18], [-24.5, 23], [-13, 24], [7, 24], [29, 21], [31, -5], [13, -24], [-8, -24], [-27, -21], [-33, -10]]; @@ -705,7 +746,8 @@ export function buildCampoMorro(scene, T = {}) { return { ambience,sound:{loops:[{src:AMB_LOOPS.funk,pos:[28,2,-21],radius:24,vol:.5},{src:AMB_LOOPS.grilos,pos:[0,3,0],radius:80,vol:.26}],bioma:'campo'}, root, colliders, occluders, decalSolids: [root], groundHeightAt, spawns, sun, hemi, pickups, ctfPoints, - update(dt) { updateVento(dt); updatePoeira(dt); }, + onGoal() { golAte = tempoTorcida + 3.2; }, + update(dt, time = 0) { tempoTorcida = time; updateVento(dt); updatePoeira(dt); atualizarTorcida(time); }, waypoints: { nodes, adj }, nearestWaypoint, findPath, levels: [{ nome: 'galpao', x0: GALPAO.x0, x1: GALPAO.x1, z0: GALPAO.z0, z1: GALPAO.z1, dePartida: 'B' }], bounds: { minX: -HALF_X + 0.5, maxX: HALF_X - 0.5, minZ: -HALF_Z + 0.5, maxZ: HALF_Z - 0.5 }, diff --git a/public/js/maps.js b/public/js/maps.js index cb3b74c6..4bda0e81 100644 --- a/public/js/maps.js +++ b/public/js/maps.js @@ -5,7 +5,7 @@ import { buildHavan, havanPropsForMatch } from './map_havan.js'; import { buildFerroVelho, FERRO_PROPS } from './map_ferrovelho.js'; import { buildQuebrada, QUEBRADA_PROPS } from './map_quebrada.js'; import { buildEscadao, ESCADAO_PROPS, ESCADAO_AMBIENCE } from './map_escadao.js'; -import { buildCampoMorro, CAMPOMORRO_PROPS } from './map_campomorro.js'; +import { buildCampoMorro, CAMPOMORRO_PROPS, CAMPOMORRO_AMBIENCE } from './map_campomorro.js'; import { buildLajes, LAJES_PROPS, LAJES_AMBIENCE } from './map_lajes_authored.js'; import { buildCorrego, CORREGO_PROPS, CORREGO_AMBIENCE } from './map_corrego.js'; import { buildMansao, MANSAO_PROPS } from './map_mansao.js'; @@ -67,7 +67,7 @@ export const MAPS = { /* DÍVIDA: renomear o prefixo `fy_` só aqui quebra CALADO — `LOOKS` (bloom.js) cai no DEFAULT_LOOK de Brasília. Renome exige mudança própria com régua (PR #200). */ escadao: { name: 'Escadão (Morro)', build: buildEscadao, props: ESCADAO_PROPS, ambience: ESCADAO_AMBIENCE, ctfMode: true }, - campomorro: { name: 'Campo do Morro', build: buildCampoMorro, props: CAMPOMORRO_PROPS, ctfMode: true }, + campomorro: { name: 'Campo do Morro', build: buildCampoMorro, props: CAMPOMORRO_PROPS, ambience: CAMPOMORRO_AMBIENCE, ctfMode: true }, lajes: { name: 'Lajes (Comunidade)', build: buildLajes, props: LAJES_PROPS, ambience: LAJES_AMBIENCE, ctfMode: true }, corrego: { name: 'Córrego (Favela de SP)', build: buildCorrego, props: CORREGO_PROPS, ambience: CORREGO_AMBIENCE, ctfMode: true }, mansao: { name: 'Mansão do Joá', build: buildMansao, props: MANSAO_PROPS, ctfMode: true }, diff --git a/tools/eval/campo-contract-check.mjs b/tools/eval/campo-contract-check.mjs index 58aae75c..0053f4c6 100644 --- a/tools/eval/campo-contract-check.mjs +++ b/tools/eval/campo-contract-check.mjs @@ -13,6 +13,7 @@ luzes e prova que a régua não confunde geometria presente com interior legível. */ import { THREE, bootGame, initTextures } from './harness.mjs'; +import { readFileSync } from 'node:fs'; import { CAMPO_FIELD_MOUTH } from './map-evidence-views.mjs'; const MUT = process.argv.includes('--mutante=spawn-fora'); @@ -26,9 +27,10 @@ const MUT_BLOCKOUT = process.argv.includes('--mutante=galpao-blockout'); const MUT_REDES = process.argv.includes('--mutante=traves-sem-rede'); const MUT_BANCO = process.argv.includes('--mutante=bancada-central'); const MUT_MARCOS = process.argv.includes('--mutante=marcos-clonados'); +const MUT_TORCIDA = process.argv.includes('--mutante=torcida-sentada'); const game = bootGame('campomorro', { textures: initTextures(), bots: 0 }); const luzesGalpao = [], faixasSaida = [], superficiesGalpao = [], framesGalpao = []; -const redes = [], bancos = [], marcos = []; +const redes = [], bancos = [], marcos = [], coberturas = [], bancosArq = [], torcidas = []; game.scene.traverse((object) => { if (object.userData?.mapLight === 'galpao') luzesGalpao.push(object); if (object.userData?.galpaoExitBand) faixasSaida.push(object); @@ -37,6 +39,9 @@ game.scene.traverse((object) => { if (object.userData?.goalNet) redes.push(object); if (object.userData?.fieldBench) bancos.push(object); if (object.userData?.fieldLandmark) marcos.push(object); + if (object.userData?.fieldStandRoof) coberturas.push(object); + if (object.userData?.fieldStandBench !== undefined) bancosArq.push(object); + if (object.userData?.crowdInstanced) torcidas.push(object); }); if (MUT_GALPAO) { for (const luz of luzesGalpao) luz.intensity = 0; @@ -46,6 +51,7 @@ if (MUT_BLOCKOUT) for (const superficie of superficiesGalpao) superficie.materia if (MUT_REDES) for (const rede of redes) rede.visible = false; if (MUT_BANCO && bancos[0]) bancos[0].position.set(0, bancos[0].position.y, 0); if (MUT_MARCOS) for (const marco of marcos) marco.userData.fieldLandmark = 'clonado'; +if (MUT_TORCIDA) game.world.onGoal = () => {}; const spawns = JSON.parse(JSON.stringify(game.world.spawns)); if (MUT) spawns.E[0] = { ...spawns.E[0], x: -31, z: 8 }; @@ -67,6 +73,16 @@ if (bancos.some((banco) => Math.abs(banco.position.x) < 3 && Math.abs(banco.posi falhas.push('bancada invade a faixa central de 6 × 6 m'); if (new Set(marcos.map((marco) => marco.userData.fieldLandmark)).size < 3) falhas.push(`${new Set(marcos.map((m) => m.userData.fieldLandmark)).size}/3 marcos distintos ao redor do campo`); +game.world.update?.(1 / 60, 0); +const torcidaSentada = torcidas.length === 1 && torcidas[0].userData.crowdState === 'seated'; +game.world.onGoal?.(); game.world.update?.(1 / 60, .5); +const torcidaNoGol = torcidas.length === 1 && torcidas[0].userData.crowdState === 'goal-standing'; +if (coberturas.filter((o) => o.visible !== false).length < 1 || bancosArq.filter((o) => o.visible !== false).length < 3) + falhas.push(`${coberturas.filter((o) => o.visible !== false).length}/1 cobertura e ${bancosArq.filter((o) => o.visible !== false).length}/3 bancos na arquibancada`); +if (!torcidaSentada || !torcidaNoGol || torcidas[0]?.count !== 24) + falhas.push(`torcida instanciada reage ao gol: sentada=${torcidaSentada} gol=${torcidaNoGol} instâncias=${torcidas[0]?.count || 0}`); +if (!readFileSync('public/js/game.js', 'utf8').includes('this.world.onGoal?.();')) + falhas.push('Game._ctfWin não chama world.onGoal()'); const noCampo = ({ x, z }) => Math.abs(x) <= 19.4 && Math.abs(z) <= 11.9; const noGalpao = ({ x, z }) => x >= 22.4 && x <= 33.6 && z >= -25.6 && z <= -16.4; for (const [i, spawn] of (spawns.E || []).entries()) if (!noCampo(spawn)) From c435bd26511e8ffef698e31c8032666b14e65201 Mon Sep 17 00:00:00 2001 From: rubenmarcus Date: Tue, 25 Aug 2026 03:41:18 +0100 Subject: [PATCH 2/2] chore(arch): reindexa evento de torcida Agent: Codex Signed-off-by: rubenmarcus --- tools/eval/ARCH.md | 138 ++++++++++++++++++++++----------------------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/tools/eval/ARCH.md b/tools/eval/ARCH.md index b5f90fe5..f08b7954 100644 --- a/tools/eval/ARCH.md +++ b/tools/eval/ARCH.md @@ -9,7 +9,7 @@ | Arquivo | Linhas | Símbolos | |---|---:|---:| -| `public/js/game.js` | 7261 | 273 | +| `public/js/game.js` | 7262 | 273 | | `public/js/main.js` | 2813 | 259 | | `public/js/glbchars.js` | 969 | 68 | | `public/js/characters.js` | 1169 | 41 | @@ -23,21 +23,21 @@ Os 15 maiores somam **3152 linhas (43% do arquivo)**. Método grande = PR irrevi | Linhas | Início | Método | | |---:|---:|---|---| -| 814 | 5903 | `_updateBot()` | ⚠️ candidato a extração | +| 814 | 5904 | `_updateBot()` | ⚠️ candidato a extração | | 547 | 612 | `constructor()` | 🔴 append-only | -| 351 | 5092 | `_updatePlayer()` | ⚠️ candidato a extração | +| 351 | 5093 | `_updatePlayer()` | ⚠️ candidato a extração | | 249 | 2316 | `_resetPositions()` | | | 241 | 1256 | `_buildViewModels()` | | -| 148 | 5443 | `_updatePickups()` | | -| 133 | 4576 | `_botCtf()` | | +| 148 | 5444 | `_updatePickups()` | | +| 133 | 4577 | `_botCtf()` | | | 115 | 1934 | `_touchControls()` | | | 85 | 3108 | `_tryShoot()` | | | 84 | 4307 | `_initCTF()` | | -| 80 | 7063 | `_updateHud()` | | +| 80 | 7064 | `_updateHud()` | | | 79 | 3499 | `_dmgArc()` | | -| 79 | 7143 | `update()` | 🔴 append-only | -| 76 | 4715 | `_updateCtfHud()` | | -| 71 | 6730 | `_updateBotNN()` | | +| 79 | 7144 | `update()` | 🔴 append-only | +| 76 | 4716 | `_updateCtfHud()` | | +| 71 | 6731 | `_updateBotNN()` | | ## Tabela de CONFLITO — resolvida para as linhas de hoje @@ -48,19 +48,19 @@ faixas disjuntas simultaneamente com zero conflito de conteúdo. | Frente | Faixas em `game.js` | Arquivos exclusivos | |---|---|---| | **ARMAS / VIEWMODEL** | `324–327` `361–455` `482–503` `1256–1654` `2873–2878` `2960–3043` `3062–3258` `3720–3744` `3792–3856` `3924–3940` | `public/js/vmattach.js` `public/js/springs.js` `public/js/weapons.js` `public/js/fparms.js` `public/js/handik.js` `public/js/recoil.js` `public/js/vmlab.js` | -| **BOTS / JOGABILIDADE** | `164–167` `218–218` `244–255` `545–556` `3360–3468` `4251–4306` `4472–4708` `4791–4813` `5092–5442` `5775–5792` `5874–6716` | — | -| **MAPAS / MUNDO** | `1202–1255` `2316–2564` `4307–4449` `5443–5590` | `public/js/maps.js` `public/js/mapprops.js` `public/js/map_brasilia.js` `public/js/map_havan.js` `public/js/map_piscina.js` `public/js/map_piscinao_ramos.js` `public/js/map_ferrovelho.js` | +| **BOTS / JOGABILIDADE** | `164–167` `218–218` `244–255` `545–556` `3360–3468` `4251–4306` `4473–4709` `4792–4814` `5093–5443` `5776–5793` `5875–6717` | — | +| **MAPAS / MUNDO** | `1202–1255` `2316–2564` `4307–4449` `5444–5591` | `public/js/maps.js` `public/js/mapprops.js` `public/js/map_brasilia.js` `public/js/map_havan.js` `public/js/map_piscina.js` `public/js/map_piscinao_ramos.js` `public/js/map_ferrovelho.js` | | **GRÁFICOS / FX** | `1655–1664` `1779–1810` `2803–2815` `3745–3783` `3867–3923` | `public/js/bloom.js` `public/js/textures.js` `public/js/vao.js` `public/js/stylize.js` `public/js/gpuparticles.js` | -| **UI / HUD / MENU** | `1159–1201` `2757–2781` `2797–2802` `2816–2822` `3499–3640` `3656–3719` `6885–6948` `6979–7027` `7063–7142` | `public/js/main.js` `public/style.css` `src/pages/index.astro` | +| **UI / HUD / MENU** | `1159–1201` `2757–2781` `2797–2802` `2816–2822` `3499–3640` `3656–3719` `6886–6949` `6980–7028` `7064–7143` | `public/js/main.js` `public/style.css` `src/pages/index.astro` | | **ÁUDIO** | — | `public/js/audio.js` | | **PERSONAGENS** | — | `public/js/characters.js` `public/js/glbchars.js` | | **SITE / BACKEND** | — | `src/` `supabase/` | -**🔴 Zonas vermelhas (append-only, qualquer frente pode precisar):** `update()` 7143–7221 · `_dom()` 1159–1201 · `constructor()` 612–1158 +**🔴 Zonas vermelhas (append-only, qualquer frente pode precisar):** `update()` 7144–7222 · `_dom()` 1159–1201 · `constructor()` 612–1158 Nenhuma sobreposição entre frentes — todas as faixas são disjuntas. ✓ -Cobertura: **3779 de 7261 linhas (52%)** do `game.js` têm dono declarado. O resto é território neutro — declare a frente mesmo assim. +Cobertura: **3779 de 7262 linhas (52%)** do `game.js` têm dono declarado. O resto é território neutro — declare a frente mesmo assim.
Índice completo de game.js (todos os símbolos) @@ -241,61 +241,61 @@ Cobertura: **3779 de 7261 linhas (52%)** do `game.js` têm dono declarado. O res | 4251 | `_botSeparation()` | 56 | | 4307 | `_initCTF()` | 84 | | 4391 | `_updateCTF()` | 59 | -| 4450 | `_ctfWin()` | 22 | -| 4472 | `_freeYaw()` | 25 | -| 4497 | `_pullString()` | 23 | -| 4520 | `_walkReach()` | 18 | -| 4538 | `_wpComp()` | 16 | -| 4554 | `_findPathLocal()` | 22 | -| 4576 | `_botCtf()` | 133 | -| 4709 | `_hideCtfHud()` | 6 | -| 4715 | `_updateCtfHud()` | 76 | -| 4791 | `_collide()` | 23 | -| 4814 | `_collideRot()` | 22 | -| 4836 | `_mantleAlcance()` | 50 | -| 4886 | `_mantleAlcancavel()` | 12 | -| 4898 | `_mantleTarget()` | 35 | -| 4933 | `_freeSpot()` | 30 | -| 4963 | `_retaAndavel()` | 20 | -| 4983 | `_walkDepth()` | 16 | -| 4999 | `_noteHit()` | 17 | -| 5016 | `_deathFeedback()` | 43 | -| 5059 | `_updateReplayCam()` | 33 | -| 5092 | `_updatePlayer()` | 351 | -| 5443 | `_updatePickups()` | 148 | -| 5591 | `_wpnMode()` | 5 | -| 5596 | `_botWeapon()` | 10 | -| 5606 | `_municaoInfinita()` | 1 | -| 5607 | `_pickupAllowed()` | 7 | -| 5614 | `_grabPickup()` | 35 | -| 5649 | `_assentarNoChao()` | 11 | -| 5660 | `_dropWeapon()` | 18 | -| 5678 | `_sumirDrop()` | 36 | -| 5714 | `_spawnY()` | 3 | -| 5717 | `_spawnYaw()` | 5 | -| 5722 | `_pickSpawn()` | 23 | -| 5745 | `_respawnPlayer()` | 30 | -| 5775 | `_losClear()` | 18 | -| 5793 | `_botCall()` | 37 | -| 5830 | `_teamMarkTex()` | 23 | -| 5853 | `_makeTeamMark()` | 14 | -| 5867 | `_updateTeamMark()` | 7 | -| 5874 | `_botEye()` | 1 | -| 5875 | `_enemyOf()` | 8 | -| 5883 | `_duelToken()` | 20 | -| 5903 | `_updateBot()` | 814 | -| 6717 | `_flushTraining()` | 13 | -| 6730 | `_updateBotNN()` | 71 | -| 6801 | `_botShootNN()` | 46 | -| 6847 | `_radarFoot()` | 38 | -| 6885 | `_updateRadar()` | 64 | -| 6949 | `_banner()` | 26 | -| 6975 | `_resultadoDaRodada()` | 4 | -| 6979 | `_showScoreboard()` | 49 | -| 7028 | `_updateWeaponHud()` | 35 | -| 7063 | `_updateHud()` | 80 | -| 7143 | `update()` | 79 | -| 7222 | `dispose()` | 39 | +| 4450 | `_ctfWin()` | 23 | +| 4473 | `_freeYaw()` | 25 | +| 4498 | `_pullString()` | 23 | +| 4521 | `_walkReach()` | 18 | +| 4539 | `_wpComp()` | 16 | +| 4555 | `_findPathLocal()` | 22 | +| 4577 | `_botCtf()` | 133 | +| 4710 | `_hideCtfHud()` | 6 | +| 4716 | `_updateCtfHud()` | 76 | +| 4792 | `_collide()` | 23 | +| 4815 | `_collideRot()` | 22 | +| 4837 | `_mantleAlcance()` | 50 | +| 4887 | `_mantleAlcancavel()` | 12 | +| 4899 | `_mantleTarget()` | 35 | +| 4934 | `_freeSpot()` | 30 | +| 4964 | `_retaAndavel()` | 20 | +| 4984 | `_walkDepth()` | 16 | +| 5000 | `_noteHit()` | 17 | +| 5017 | `_deathFeedback()` | 43 | +| 5060 | `_updateReplayCam()` | 33 | +| 5093 | `_updatePlayer()` | 351 | +| 5444 | `_updatePickups()` | 148 | +| 5592 | `_wpnMode()` | 5 | +| 5597 | `_botWeapon()` | 10 | +| 5607 | `_municaoInfinita()` | 1 | +| 5608 | `_pickupAllowed()` | 7 | +| 5615 | `_grabPickup()` | 35 | +| 5650 | `_assentarNoChao()` | 11 | +| 5661 | `_dropWeapon()` | 18 | +| 5679 | `_sumirDrop()` | 36 | +| 5715 | `_spawnY()` | 3 | +| 5718 | `_spawnYaw()` | 5 | +| 5723 | `_pickSpawn()` | 23 | +| 5746 | `_respawnPlayer()` | 30 | +| 5776 | `_losClear()` | 18 | +| 5794 | `_botCall()` | 37 | +| 5831 | `_teamMarkTex()` | 23 | +| 5854 | `_makeTeamMark()` | 14 | +| 5868 | `_updateTeamMark()` | 7 | +| 5875 | `_botEye()` | 1 | +| 5876 | `_enemyOf()` | 8 | +| 5884 | `_duelToken()` | 20 | +| 5904 | `_updateBot()` | 814 | +| 6718 | `_flushTraining()` | 13 | +| 6731 | `_updateBotNN()` | 71 | +| 6802 | `_botShootNN()` | 46 | +| 6848 | `_radarFoot()` | 38 | +| 6886 | `_updateRadar()` | 64 | +| 6950 | `_banner()` | 26 | +| 6976 | `_resultadoDaRodada()` | 4 | +| 6980 | `_showScoreboard()` | 49 | +| 7029 | `_updateWeaponHud()` | 35 | +| 7064 | `_updateHud()` | 80 | +| 7144 | `update()` | 79 | +| 7223 | `dispose()` | 39 |