feat(jogo): expediente dos mortos, jogo de zumbis em primeira pessoa
CI / audit (push) Successful in 10m48s
CI / verify (push) Successful in 19m2s
CI / deploy (push) Successful in 2m14s
CI / e2e (push) Failing after 36m15s

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.
This commit is contained in:
evandro committed 2026-11-10 19:37:10 -03:00
1 parent c7d09d63d4
commit 67b32fcab3
118 files changed
+16730

No files matched your search

+714
View File
@@ -0,0 +1,714 @@
/**
* 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();
},
};
}