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.
715 lines
22 KiB
TypeScript
715 lines
22 KiB
TypeScript
/**
|
||
* 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();
|
||
},
|
||
};
|
||
}
|