Files
portifolio-evandrodevbr/lib/jogo/armas.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

207 lines
5.7 KiB
TypeScript

/** 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);
}