/** * Efeitos da cena: arma na tela, clarão do cano, traçante, sangue e os sons ligados aos eventos da lógica. * Tudo em reserva fixa e sem alocação por quadro em `avancar`. São 5 chamadas de desenho no total: * arma, clarão, traçantes, gotas e marcas. */ import { AdditiveBlending, BoxGeometry, BufferGeometry, CanvasTexture, Color, DynamicDrawUsage, Float32BufferAttribute, Group, InstancedMesh, Matrix4, Mesh, MeshBasicMaterial, MeshStandardMaterial, type PerspectiveCamera, PlaneGeometry, PointLight, Quaternion, type Scene, Vector3, } from "three"; import type { ArmaId, Evento, Partida } from "@/lib/jogo/tipos"; import { JOGADOR } from "@/lib/jogo/valores"; import type { Som } from "./som"; export type Efeitos = { aoEvento(evento: Evento, p: Partida): void; /** `dt` em segundos. */ avancar(dt: number, p: Partida): void; descartar(): void; }; /** Durações em segundos. */ const CLARAO = 0.06; const TRACANTE = 0.06; const VIDA_DA_GOTA = 0.5; const TRANCO = 0.15; const CRESCIMENTO_DA_POCA = 1.2; const MAX_TRACANTES = 8; const MAX_GOTAS = 64; const MAX_MARCAS = 64; const GRAVIDADE = 9.8; /** A mesma altura do modelo do drone em `atores.ts`. */ const ALTURA_DO_DRONE = 2.3; /** A mesma altura de acerto do drone em `lib/jogo/drones.ts`. */ const ALTURA_DO_ACERTO_DO_DRONE = 1.2; const ALCANCE_DO_GEMIDO = 12; const ESPESSURA_DO_TRACANTE = 0.012; const COR_DO_FERROLHO = 0x464a51; const COR_DA_ARMACAO = 0x2c2f34; const COR_ESCURA = 0x17181b; const COR_DA_LAMINA = 0xb4bac2; const COR_DO_FIO = 0xdde2e8; const COR_DA_GOTA = 0x6a0a0a; const COR_DA_MARCA = 0x3b0505; const COR_DO_FOGO = 0xffc46b; /** Onde a arma fica no espaço da câmera (x à direita, y para cima, −z para a frente): baixa, no canto direito. */ const POSICAO_DA_ARMA = new Vector3(0.2, -0.21, -0.44); /** Giro de repouso em y: o cano aponta um pouco para dentro, na direção da mira. */ const GIRO_DA_ARMA = 0.07; /** Uma caixa do modelo: largura, altura, profundidade, centro (x, y, z), cor e giro em x. */ type Peca = readonly [ number, number, number, number, number, number, number, number?, ]; /** Pistola de 19 cm: ferrolho, cano, armação, guarda-mato e cabo inclinado. */ const PISTOLA: readonly Peca[] = [ [0.026, 0.03, 0.186, 0, 0.024, -0.068, COR_DO_FERROLHO], [0.012, 0.012, 0.008, 0, 0.026, -0.163, COR_ESCURA], [0.002, 0.011, 0.04, 0.0132, 0.029, -0.04, COR_ESCURA], [0.018, 0.006, 0.006, 0, 0.042, 0.018, COR_ESCURA], [0.004, 0.006, 0.005, 0, 0.042, -0.154, COR_ESCURA], [0.024, 0.018, 0.16, 0, 0, -0.06, COR_DA_ARMACAO], [0.012, 0.006, 0.052, 0, -0.03, -0.058, COR_DA_ARMACAO], [0.012, 0.03, 0.007, 0, -0.016, -0.084, COR_DA_ARMACAO], [0.005, 0.018, 0.005, 0, -0.014, -0.05, COR_ESCURA, 0.2], [0.028, 0.105, 0.048, 0, -0.055, 0.008, COR_DA_ARMACAO, -0.32], [0.026, 0.012, 0.02, 0, -0.004, 0.03, COR_DA_ARMACAO], [0.03, 0.01, 0.052, 0, -0.108, 0.027, COR_ESCURA, -0.32], ]; /** Automática: mais comprida, com carregador e coronha curta vazada. */ const AUTOMATICA: readonly Peca[] = [ [0.034, 0.05, 0.24, 0, 0.02, -0.08, COR_DO_FERROLHO], [0.014, 0.008, 0.22, 0, 0.049, -0.1, COR_ESCURA], [0.02, 0.016, 0.02, 0, 0.06, 0.01, COR_ESCURA], [0.006, 0.02, 0.008, 0, 0.056, -0.29, COR_ESCURA], [0.032, 0.04, 0.11, 0, 0.015, -0.255, COR_DA_ARMACAO], [0.014, 0.014, 0.06, 0, 0.02, -0.34, COR_ESCURA], [0.02, 0.02, 0.03, 0, 0.02, -0.385, COR_ESCURA], [0.022, 0.12, 0.04, 0, -0.06, -0.13, COR_ESCURA, 0.15], [0.028, 0.09, 0.04, 0, -0.04, 0, COR_DA_ARMACAO, -0.3], [0.012, 0.006, 0.05, 0, -0.022, -0.045, COR_DA_ARMACAO], [0.008, 0.008, 0.12, 0, 0.03, 0.1, COR_ESCURA], [0.008, 0.008, 0.12, 0, -0.01, 0.1, COR_ESCURA], [0.03, 0.07, 0.012, 0, 0.01, 0.16, COR_DA_ARMACAO], ]; /** Faca: lâmina de aço com fio mais claro, guarda e cabo escuro. */ const FACA: readonly Peca[] = [ [0.024, 0.03, 0.11, 0, -0.005, 0.02, COR_ESCURA], [0.012, 0.05, 0.012, 0, 0, -0.04, COR_DO_FERROLHO], [0.004, 0.03, 0.16, 0, 0.002, -0.126, COR_DA_LAMINA], [0.0042, 0.006, 0.16, 0, -0.011, -0.126, COR_DO_FIO], [0.004, 0.018, 0.04, 0, 0.008, -0.222, COR_DA_LAMINA], ]; const MODELOS = [PISTOLA, AUTOMATICA, FACA] as const; const NOMES_DOS_MODELOS = ["pistola", "automatica", "faca"] as const; /** Boca do cano de cada arma, no espaço do grupo da arma. */ const BOCAIS: readonly Vector3[] = [ new Vector3(0, 0.026, -0.17), new Vector3(0, 0.02, -0.405), new Vector3(0, 0, -0.25), ]; const TAMANHO_DO_CLARAO = [0.1, 0.13, 0]; /** Coice por arma: recuo em z, giro em x e em y. A faca avança e corta de lado. */ const COICES: readonly (readonly [number, number, number])[] = [ [0.045, 0.16, 0], [0.02, 0.05, 0], [-0.09, 0.1, 0.6], ]; const SONS_DAS_ARMAS = ["pistola", "automatica", "faca"] as const; /** Junta caixas numa geometria só, com a cor em cada vértice: uma chamada de desenho por arma. */ function montarModelo(pecas: readonly Peca[]): BufferGeometry { const posicoes: number[] = []; const normais: number[] = []; const cores: number[] = []; const indices: number[] = []; const cor = new Color(); for (const [l, a, pr, x, y, z, hex, giro] of pecas) { const caixa = new BoxGeometry(l, a, pr); if (giro) caixa.rotateX(giro); caixa.translate(x, y, z); const base = posicoes.length / 3; const posicao = caixa.getAttribute("position"); posicoes.push(...(posicao.array as Float32Array)); normais.push(...(caixa.getAttribute("normal").array as Float32Array)); cor.setHex(hex); for (let i = 0; i < posicao.count; i++) cores.push(cor.r, cor.g, cor.b); const indice = caixa.getIndex(); if (indice) for (const i of indice.array) indices.push(base + i); caixa.dispose(); } const geometria = new BufferGeometry(); geometria.setAttribute("position", new Float32BufferAttribute(posicoes, 3)); geometria.setAttribute("normal", new Float32BufferAttribute(normais, 3)); geometria.setAttribute("color", new Float32BufferAttribute(cores, 3)); geometria.setIndex(indices); return geometria; } function criarTela(): [HTMLCanvasElement, CanvasRenderingContext2D | null] { const tela = document.createElement("canvas"); tela.width = 64; tela.height = 64; return [tela, tela.getContext("2d")]; } /** Mancha irregular branca com gotas em volta; a cor vem do material. */ function texturaDeRespingo(): CanvasTexture { const [tela, ctx] = criarTela(); if (ctx) { ctx.fillStyle = "#ffffff"; for (let i = 0; i < 10; i++) { const angulo = (i / 10) * Math.PI * 2; const r = 4 + Math.random() * 6; ctx.beginPath(); ctx.arc( 32 + Math.cos(angulo) * r, 32 + Math.sin(angulo) * r, 9 + Math.random() * 6, 0, Math.PI * 2, ); ctx.fill(); } for (let i = 0; i < 8; i++) { const angulo = Math.random() * Math.PI * 2; const r = 18 + Math.random() * 9; ctx.beginPath(); ctx.arc( 32 + Math.cos(angulo) * r, 32 + Math.sin(angulo) * r, 1.5 + Math.random() * 2.5, 0, Math.PI * 2, ); ctx.fill(); } } return new CanvasTexture(tela); } /** Brilho redondo com uma cruz fina, para o clarão do cano. */ function texturaDeClarao(): CanvasTexture { const [tela, ctx] = criarTela(); if (ctx) { const brilho = ctx.createRadialGradient(32, 32, 0, 32, 32, 32); brilho.addColorStop(0, "rgba(255,255,255,1)"); brilho.addColorStop(0.3, "rgba(255,255,255,0.45)"); brilho.addColorStop(1, "rgba(255,255,255,0)"); ctx.fillStyle = brilho; ctx.fillRect(0, 0, 64, 64); ctx.fillStyle = "rgba(255,255,255,0.8)"; ctx.fillRect(4, 31, 56, 2); ctx.fillRect(31, 4, 2, 56); } return new CanvasTexture(tela); } function criarReserva( geometria: BufferGeometry, material: MeshBasicMaterial, quantidade: number, nome: string, ): InstancedMesh { const malha = new InstancedMesh(geometria, material, quantidade); malha.name = nome; malha.instanceMatrix.setUsage(DynamicDrawUsage); // As instâncias andam pelo mapa todo; a esfera calculada uma vez cortaria as que saem dela. malha.frustumCulled = false; for (let i = 0; i < quantidade; i++) malha.setMatrixAt(i, ESCONDIDA); return malha; } const ESCONDIDA = new Matrix4().makeScale(0, 0, 0); const EIXO_Y = new Vector3(0, 1, 0); const EIXO_Z = new Vector3(0, 0, 1); /** Reaproveitado a cada gemido para não alocar dentro de `avancar`. */ const OPCOES_DO_GEMIDO = { volume: 1, variar: true }; export function criarEfeitos( cena: Scene, camera: PerspectiveCamera, opcoes: { movimentoReduzido: boolean; som: Som }, ): Efeitos { const { movimentoReduzido, som } = opcoes; // Filhos da câmera só aparecem se a câmera estiver na cena. const colocouCamera = camera.parent === null; if (colocouCamera) cena.add(camera); // Arma na tela. const materialDaArma = new MeshStandardMaterial({ vertexColors: true, metalness: 0.45, roughness: 0.62, }); const arma = new Group(); arma.name = "efeitos-arma"; arma.position.copy(POSICAO_DA_ARMA); const modelos = MODELOS.map((pecas, i) => { const modelo = new Mesh(montarModelo(pecas), materialDaArma); modelo.name = `arma-${NOMES_DOS_MODELOS[i]}`; modelo.visible = i === 0; arma.add(modelo); return modelo; }); let armaAtual: ArmaId = 0; const texturaDoClarao = texturaDeClarao(); const clarao = new Mesh( new PlaneGeometry(1, 1), new MeshBasicMaterial({ map: texturaDoClarao, color: COR_DO_FOGO, blending: AdditiveBlending, transparent: true, depthWrite: false, toneMapped: false, }), ); clarao.name = "efeitos-clarao"; clarao.visible = false; // A luz fica sempre na cena e só muda de intensidade: ligar e desligar luzes recompila os materiais. const luz = new PointLight(COR_DO_FOGO, 0, 6, 2); luz.name = "efeitos-luz"; arma.add(clarao, luz); camera.add(arma); // Efeitos no mundo. const mundo = new Group(); mundo.name = "efeitos"; cena.add(mundo); const tracantes = criarReserva( new BoxGeometry(1, 1, 1), new MeshBasicMaterial({ color: 0xfff0b0, blending: AdditiveBlending, transparent: true, depthWrite: false, toneMapped: false, }), MAX_TRACANTES, "efeitos-tracantes", ); const respingo = texturaDeRespingo(); const gotas = criarReserva( new PlaneGeometry(1, 1), new MeshBasicMaterial({ map: respingo, color: COR_DA_GOTA, transparent: true, depthWrite: false, }), MAX_GOTAS, "efeitos-gotas", ); const marcas = criarReserva( new PlaneGeometry(1, 1).rotateX(-Math.PI / 2), new MeshBasicMaterial({ map: respingo, color: COR_DA_MARCA, transparent: true, opacity: 0.9, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -4, }), MAX_MARCAS, "efeitos-marcas", ); const cor = new Color(); for (let i = 0; i < MAX_TRACANTES; i++) tracantes.setColorAt(i, cor.setScalar(1)); mundo.add(marcas, gotas, tracantes); // Estado em reserva fixa. const vidaDoTracante = new Float32Array(MAX_TRACANTES); let proximoTracante = 0; let tracantesVivos = 0; const posicaoDaGota = new Float32Array(MAX_GOTAS * 3); const velocidadeDaGota = new Float32Array(MAX_GOTAS * 3); const vidaDaGota = new Float32Array(MAX_GOTAS); const tamanhoDaGota = new Float32Array(MAX_GOTAS); let proximaGota = 0; let gotasVivas = 0; /** x, z, ângulo e tamanho final de cada marca; `crescimento` vai de 0 a 1. */ const marca = new Float32Array(MAX_MARCAS * 4); const crescimento = new Float32Array(MAX_MARCAS).fill(1); let proximaMarca = 0; let marcasCrescendo = 0; let restoDoClarao = 0; let coice = 0; let tranco = 0; let fase = 0; let amplitude = 0; let ultimoX = Number.NaN; let ultimoZ = Number.NaN; let origemX = 0; let origemZ = 0; let proximoGemido = 2 + Math.random() * 3; const trancoAplicado = new Vector3(); let rolAplicado = 0; // Temporários reaproveitados. const a = new Vector3(); const b = new Vector3(); const escala = new Vector3(); const direcao = new Vector3(); const meio = new Vector3(); const giro = new Quaternion(); const matriz = new Matrix4(); function trocarArma(nova: ArmaId): void { modelos[armaAtual].visible = false; modelos[nova].visible = true; armaAtual = nova; clarao.position.copy(BOCAIS[nova]); luz.position.copy(BOCAIS[nova]); } trocarArma(0); function lancarTracante(de: Vector3, ate: Vector3): void { const i = proximoTracante; proximoTracante = (i + 1) % MAX_TRACANTES; if (vidaDoTracante[i] <= 0) tracantesVivos++; vidaDoTracante[i] = TRACANTE; direcao.subVectors(ate, de); const comprimento = direcao.length(); if (comprimento < 1e-3) tracantes.setMatrixAt(i, ESCONDIDA); else { giro.setFromUnitVectors(EIXO_Z, direcao.divideScalar(comprimento)); escala.set(ESPESSURA_DO_TRACANTE, ESPESSURA_DO_TRACANTE, comprimento); meio.addVectors(de, ate).multiplyScalar(0.5); tracantes.setMatrixAt(i, matriz.compose(meio, giro, escala)); } tracantes.setColorAt(i, cor.setScalar(1)); tracantes.instanceMatrix.needsUpdate = true; if (tracantes.instanceColor) tracantes.instanceColor.needsUpdate = true; } function pintarMarca(i: number): void { const k = i * 4; const s = marca[k + 3] * (0.25 + 0.75 * crescimento[i]); a.set(marca[k], 0.012, marca[k + 1]); giro.setFromAxisAngle(EIXO_Y, marca[k + 2]); escala.set(s, 1, s); marcas.setMatrixAt(i, matriz.compose(a, giro, escala)); marcas.instanceMatrix.needsUpdate = true; } function deixarMarca( x: number, z: number, tamanho: number, crescer: boolean, ): void { const i = proximaMarca; proximaMarca = (i + 1) % MAX_MARCAS; const k = i * 4; marca[k] = x; marca[k + 1] = z; marca[k + 2] = Math.random() * Math.PI * 2; marca[k + 3] = tamanho; if (crescimento[i] < 1) marcasCrescendo--; crescimento[i] = crescer ? 0 : 1; if (crescer) marcasCrescendo++; pintarMarca(i); } function zumbiPorId(p: Partida, id: number) { for (const zumbi of p.zumbis) if (zumbi.id === id) return zumbi; return undefined; } function espirrar(x: number, y: number, z: number, quantidade: number): void { let dx = x - origemX; let dz = z - origemZ; const d = Math.hypot(dx, dz); if (d > 1e-3) { dx /= d; dz /= d; } for (let n = 0; n < quantidade; n++) { const i = proximaGota; proximaGota = (i + 1) % MAX_GOTAS; if (vidaDaGota[i] <= 0) gotasVivas++; vidaDaGota[i] = VIDA_DA_GOTA; tamanhoDaGota[i] = 0.03 + Math.random() * 0.05; const velocidade = 1.2 + Math.random() * 2.3; const k = i * 3; posicaoDaGota[k] = x; posicaoDaGota[k + 1] = y; posicaoDaGota[k + 2] = z; velocidadeDaGota[k] = dx * velocidade + (Math.random() - 0.5) * 1.6; velocidadeDaGota[k + 1] = 0.6 + Math.random() * 1.8; velocidadeDaGota[k + 2] = dz * velocidade + (Math.random() - 0.5) * 1.6; } } function atirou(evento: Extract, p: Partida): void { if (evento.autor === "jogador") { const j = p.jogador; origemX = j.x; origemZ = j.z; const qual = evento.arma === "drone" ? armaAtual : evento.arma; if (qual !== armaAtual) trocarArma(qual); coice = 1; som.tocar(SONS_DAS_ARMAS[qual], { variar: true }); if (qual === 2) return; restoDoClarao = CLARAO; clarao.visible = true; clarao.rotation.z = Math.random() * Math.PI; clarao.scale.setScalar( TAMANHO_DO_CLARAO[qual] * (0.8 + Math.random() * 0.4), ); luz.intensity = 4; camera.updateMatrixWorld(); a.copy(BOCAIS[qual]).applyMatrix4(arma.matrixWorld); const t = evento.distancia; b.set( j.x + Math.sin(j.rumo) * t, JOGADOR.olhos + Math.tan(j.inclinacao) * t, j.z + Math.cos(j.rumo) * t, ); lancarTracante(a, b); return; } som.tocar("drone", { volume: 0.35, variar: true }); const drone = p.drones.find((d) => d.id === evento.autor); if (!drone) return; origemX = drone.x; origemZ = drone.z; const t = evento.distancia; a.set(drone.x, ALTURA_DO_DRONE, drone.z); b.set( drone.x + Math.sin(drone.rumo) * t, ALTURA_DO_ACERTO_DO_DRONE, drone.z + Math.cos(drone.rumo) * t, ); lancarTracante(a, b); } function avancarTracantes(dt: number): void { if (tracantesVivos === 0) return; for (let i = 0; i < MAX_TRACANTES; i++) { if (vidaDoTracante[i] <= 0) continue; vidaDoTracante[i] -= dt; if (vidaDoTracante[i] <= 0) { tracantesVivos--; tracantes.setMatrixAt(i, ESCONDIDA); } else tracantes.setColorAt(i, cor.setScalar(vidaDoTracante[i] / TRACANTE)); } tracantes.instanceMatrix.needsUpdate = true; if (tracantes.instanceColor) tracantes.instanceColor.needsUpdate = true; } function avancarGotas(dt: number): void { if (gotasVivas === 0) return; for (let i = 0; i < MAX_GOTAS; i++) { if (vidaDaGota[i] <= 0) continue; vidaDaGota[i] -= dt; if (vidaDaGota[i] <= 0) { gotasVivas--; gotas.setMatrixAt(i, ESCONDIDA); continue; } const k = i * 3; velocidadeDaGota[k + 1] -= GRAVIDADE * dt; posicaoDaGota[k] += velocidadeDaGota[k] * dt; posicaoDaGota[k + 1] += velocidadeDaGota[k + 1] * dt; posicaoDaGota[k + 2] += velocidadeDaGota[k + 2] * dt; if (posicaoDaGota[k + 1] < 0.02) { posicaoDaGota[k + 1] = 0.02; velocidadeDaGota[k] = 0; velocidadeDaGota[k + 1] = 0; velocidadeDaGota[k + 2] = 0; } const s = tamanhoDaGota[i] * (0.5 + vidaDaGota[i] / VIDA_DA_GOTA); a.set(posicaoDaGota[k], posicaoDaGota[k + 1], posicaoDaGota[k + 2]); escala.set(s, s, s); gotas.setMatrixAt(i, matriz.compose(a, camera.quaternion, escala)); } gotas.instanceMatrix.needsUpdate = true; } function avancarMarcas(dt: number): void { if (marcasCrescendo === 0) return; for (let i = 0; i < MAX_MARCAS; i++) { if (crescimento[i] >= 1) continue; crescimento[i] = Math.min(1, crescimento[i] + dt / CRESCIMENTO_DA_POCA); if (crescimento[i] >= 1) marcasCrescendo--; pintarMarca(i); } } function avancarArma(dt: number, p: Partida): void { if (p.jogador.arma !== armaAtual) trocarArma(p.jogador.arma); if (restoDoClarao > 0) { restoDoClarao -= dt; if (restoDoClarao <= 0) { clarao.visible = false; luz.intensity = 0; } } const { x, z } = p.jogador; const andou = Math.hypot(x - ultimoX, z - ultimoZ); ultimoX = x; ultimoZ = z; const andando = !movimentoReduzido && dt > 0 && andou / dt > 0.5 && andou < 1; amplitude += ((andando ? 1 : 0) - amplitude) * Math.min(1, dt * 8); if (andando) fase += dt * 9; coice *= Math.exp(-dt * 14); const [recuo, giroX, giroY] = COICES[armaAtual]; arma.position.set( POSICAO_DA_ARMA.x + Math.sin(fase) * 0.012 * amplitude, POSICAO_DA_ARMA.y - Math.abs(Math.cos(fase)) * 0.01 * amplitude, POSICAO_DA_ARMA.z + recuo * coice, ); arma.rotation.set(giroX * coice, GIRO_DA_ARMA + giroY * coice, 0); } /** O tranco é somado por cima de quem posiciona a câmera e retirado no quadro seguinte. */ function avancarTranco(dt: number): void { if (tranco <= 0 && rolAplicado === 0 && trancoAplicado.lengthSq() === 0) return; camera.position.sub(trancoAplicado); camera.rotation.z -= rolAplicado; tranco -= dt; if (tranco > 0) { const forca = tranco / TRANCO; trancoAplicado.set( (Math.random() - 0.5) * 0.08 * forca, (Math.random() - 0.5) * 0.06 * forca, 0, ); rolAplicado = (Math.random() - 0.5) * 0.06 * forca; } else { trancoAplicado.set(0, 0, 0); rolAplicado = 0; } camera.position.add(trancoAplicado); camera.rotation.z += rolAplicado; } function avancarGemidos(dt: number, p: Partida): void { proximoGemido -= dt; if (proximoGemido > 0) return; proximoGemido = 2 + Math.random() * 3; const total = p.zumbis.length; const inicio = Math.floor(Math.random() * total); for (let n = 0; n < total; n++) { const zumbi = p.zumbis[(inicio + n) % total]; if (zumbi.estado === "morrendo") continue; const d = Math.hypot(zumbi.x - p.jogador.x, zumbi.z - p.jogador.z); if (d > ALCANCE_DO_GEMIDO) continue; OPCOES_DO_GEMIDO.volume = 1 - d / ALCANCE_DO_GEMIDO; som.tocar( Math.random() < 0.5 ? "gemido-1" : "gemido-2", OPCOES_DO_GEMIDO, ); return; } } return { aoEvento(evento, p) { switch (evento.tipo) { case "tiro": atirou(evento, p); break; case "acerto": { som.tocar("acerto", { volume: 0.7, variar: true }); espirrar(evento.x, evento.y, evento.z, evento.cabeca ? 12 : 6); const zumbi = zumbiPorId(p, evento.zumbiId); const x = zumbi ? zumbi.x : evento.x; const z = zumbi ? zumbi.z : evento.z; deixarMarca( x + (Math.random() - 0.5) * 0.4, z + (Math.random() - 0.5) * 0.4, 0.25 + Math.random() * 0.25, false, ); break; } case "morte": { const zumbi = zumbiPorId(p, evento.zumbiId); if (zumbi) deixarMarca(zumbi.x, zumbi.z, 1 + Math.random() * 0.5, true); break; } case "golpe": som.tocar("golpe"); if (!movimentoReduzido) tranco = TRANCO; break; case "resolvido": som.tocar("resolvido"); break; case "caixa": som.tocar("caixa"); break; } }, avancar(dt, p) { avancarArma(dt, p); avancarTracantes(dt); avancarGotas(dt); avancarMarcas(dt); avancarTranco(dt); avancarGemidos(dt, p); }, descartar() { camera.position.sub(trancoAplicado); camera.rotation.z -= rolAplicado; trancoAplicado.set(0, 0, 0); rolAplicado = 0; arma.removeFromParent(); mundo.removeFromParent(); if (colocouCamera) camera.removeFromParent(); for (const modelo of modelos) modelo.geometry.dispose(); materialDaArma.dispose(); for (const malha of [clarao, tracantes, gotas, marcas]) { malha.geometry.dispose(); malha.material.dispose(); } for (const malha of [tracantes, gotas, marcas]) malha.dispose(); texturaDoClarao.dispose(); respingo.dispose(); luz.dispose(); }, }; }