Files
portifolio-evandrodevbr/app/jogo/efeitos.ts
T
evandro 67b32fcab3
CI / audit (push) Successful in 10m48s
CI / verify (push) Successful in 19m2s
CI / deploy (push) Successful in 2m14s
CI / e2e (push) Failing after 36m15s
feat(jogo): expediente dos mortos, jogo de zumbis em primeira pessoa
Etapa 1 inteira (logica com 100% de cobertura, cena 3D, toque, sons, bancada de desempenho, e2e) e a base da etapa 2 (mapa novo no Tiled, texturas, imagens dos itens, sons conferidos, golpe recortado e medido). Pagina /jogo fora do menu e do sitemap ate a medicao no celular.
2026-11-10 19:37:10 -03:00

715 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<BufferGeometry, MeshBasicMaterial> {
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<Evento, { tipo: "tiro" }>, 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();
},
};
}