/** Armas do jogador: tiro por raio, faca, troca, recarga e coleta de caixas. Sem three, React nem DOM. */ import { raioAteParede } from "./mapa"; import type { ArmaId, Partida, Zumbi } from "./tipos"; import { ARCO_DA_FACA, ARMAS, FRACAO_DA_CABECA, JOGADOR, MULTIPLICADOR_DA_CABECA, MUNICAO, ZUMBIS, } from "./valores"; import { ferir } from "./zumbis"; const FACA: ArmaId = 2; const RECARGA_DA_TROCA = 10; /** Distância ao longo do raio até o círculo; `null` se não cruza ou fica atrás; 0 se a origem está dentro. */ export function raioNoCirculo( ox: number, oz: number, dx: number, dz: number, cx: number, cz: number, r: number, ): number | null { const fx = ox - cx; const fz = oz - cz; const fora = fx * fx + fz * fz - r * r; if (fora <= 0) return 0; const b = fx * dx + fz * dz; const delta = b * b - fora; if (delta < 0) return null; const t = -b - Math.sqrt(delta); return t < 0 ? null : t; } export function trocarArma(p: Partida, arma: ArmaId): void { if (arma === p.jogador.arma) return; p.jogador.arma = arma; p.jogador.recarga = RECARGA_DA_TROCA; } export function resfriar(p: Partida): void { p.jogador.recarga = Math.max(0, p.jogador.recarga - 1); } export function recolherCaixas(p: Partida): void { const { x, z } = p.jogador; const perto = (c: { x: number; z: number }) => Math.hypot(c.x - x, c.z - z) <= MUNICAO.raioDeColeta; const recolhidas = p.caixas.filter(perto); for (let i = 0; i < recolhidas.length; i++) { p.jogador.municao += MUNICAO.porCaixa; p.eventos.push({ tipo: "caixa", balas: MUNICAO.porCaixa }); } p.caixas = p.caixas.filter((c) => !perto(c)); } type Cruzamento = { zumbi: Zumbi; t: number }; /** Zumbis vivos cruzados pelo raio antes do limite, do mais perto para o mais longe. */ function cruzamentos( p: Partida, dx: number, dz: number, limite: number, ): Cruzamento[] { const lista: Cruzamento[] = []; for (const zumbi of p.zumbis) { if (zumbi.estado === "morrendo") continue; const { x, z } = p.jogador; const t = raioNoCirculo( x, z, dx, dz, zumbi.x, zumbi.z, ZUMBIS[zumbi.tipo].raio, ); if (t !== null && t < limite) lista.push({ zumbi, t }); } return lista.sort((a, b) => a.t - b.t); } /** O primeiro zumbi cuja altura contém a altura do tiro, com essa altura. */ function primeiroAtingido( p: Partida, lista: Cruzamento[], ): (Cruzamento & { y: number }) | null { const subida = Math.tan(p.jogador.inclinacao); for (const c of lista) { const y = JOGADOR.olhos + subida * c.t; if (y >= 0 && y <= ZUMBIS[c.zumbi.tipo].altura) return { ...c, y }; } return null; } /** Distância até a parede (ou o alcance da arma) no rumo atual. */ function limiteDoRaio(p: Partida): number { const { x, z, rumo, arma } = p.jogador; return raioAteParede(p.mapa, x, z, rumo, ARMAS[arma].alcance); } /** O zumbi que um tiro de raio acertaria agora; `null` com a faca ou sem alvo. */ export function alvoNaMira(p: Partida): { zumbi: Zumbi; distancia: number; cabeca: boolean; y: number; } | null { const { rumo, arma } = p.jogador; if (arma === FACA) return null; const lista = cruzamentos(p, Math.sin(rumo), Math.cos(rumo), limiteDoRaio(p)); const alvo = primeiroAtingido(p, lista); if (!alvo) return null; const { zumbi, t, y } = alvo; const cabeca = y >= FRACAO_DA_CABECA * ZUMBIS[zumbi.tipo].altura; return { zumbi, distancia: t, cabeca, y }; } function atirarPorRaio(p: Partida): void { const { x, z, rumo, arma } = p.jogador; const alvo = alvoNaMira(p); p.eventos.push({ tipo: "tiro", autor: "jogador", arma, distancia: alvo ? alvo.distancia : limiteDoRaio(p), }); if (!alvo) return; const { zumbi, distancia: t, cabeca, y } = alvo; const dx = Math.sin(rumo); const dz = Math.cos(rumo); ferir( p, zumbi, cabeca ? ARMAS[arma].dano * MULTIPLICADOR_DA_CABECA : ARMAS[arma].dano, "jogador", ); p.eventos.push({ tipo: "acerto", zumbiId: zumbi.id, cabeca, x: x + dx * t, y, z: z + dz * t, }); } /** Ângulo, de 0 a π, entre o rumo do jogador e a direção até o ponto. */ function desvio(p: Partida, zumbi: Zumbi): number { const d = Math.atan2(zumbi.x - p.jogador.x, zumbi.z - p.jogador.z) - p.jogador.rumo; return Math.abs(Math.atan2(Math.sin(d), Math.cos(d))); } /** Zumbi vivo mais próximo ao alcance da faca e dentro do arco, com a folga até a borda dele. */ function alvoDaFaca(p: Partida): { zumbi: Zumbi; folga: number } | null { let melhor: { zumbi: Zumbi; folga: number } | null = null; for (const zumbi of p.zumbis) { if (zumbi.estado === "morrendo") continue; const folga = Math.hypot(zumbi.x - p.jogador.x, zumbi.z - p.jogador.z) - ZUMBIS[zumbi.tipo].raio; const vale = folga <= ARMAS[FACA].alcance && desvio(p, zumbi) <= ARCO_DA_FACA; if (vale && (melhor === null || folga < melhor.folga)) melhor = { zumbi, folga }; } return melhor; } function golpearComFaca(p: Partida): void { const alvo = alvoDaFaca(p); p.eventos.push({ tipo: "tiro", autor: "jogador", arma: FACA, distancia: alvo ? alvo.folga : ARMAS[FACA].alcance, }); if (!alvo) return; const { zumbi } = alvo; ferir(p, zumbi, ARMAS[FACA].dano, "jogador"); p.eventos.push({ tipo: "acerto", zumbiId: zumbi.id, cabeca: false, x: zumbi.x, y: ZUMBIS[zumbi.tipo].altura / 2, z: zumbi.z, }); } export function atirar(p: Partida): void { const j = p.jogador; const arma = ARMAS[j.arma]; if (j.recarga > 0 || (arma.gasta && j.municao === 0)) return; j.recarga = arma.intervalo; if (arma.gasta) j.municao -= 1; if (j.arma === FACA) golpearComFaca(p); else atirarPorRaio(p); }