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

200 lines
5.8 KiB
TypeScript

/** Zumbis: nascer, ferir, andar, atacar e se afastar uns dos outros. Altera a `Partida` no lugar. */
import { ajustar } from "./dificuldade";
import { caminhoLivre, direcaoNoCampo, moverComColisao } from "./mapa";
import { sortear } from "./rng";
import type { Autor, Partida, Zumbi } from "./tipos";
import {
ATAQUE_DO_ZUMBI,
JOGADOR,
MUNICAO,
PASSO,
RAIO_DE_PASSAGEM,
ZUMBIS,
} from "./valores";
const INTERVALO_DE_NASCER = 20;
const DISTANCIA_MINIMA_DO_NASCEDOURO = 8;
/** Passo (dentro do intervalo) em que o golpe acontece. */
const PASSO_DO_GOLPE = 20;
/** Raio usado contra o mapa: corpo grande não pode ficar maior que a passagem. */
function raioNoMapa(zumbi: Zumbi): number {
return Math.min(ZUMBIS[zumbi.tipo].raio, RAIO_DE_PASSAGEM);
}
function distanciaAoJogador(p: Partida, x: number, z: number): number {
return Math.hypot(p.jogador.x - x, p.jogador.z - z);
}
function nascedouroPara(p: Partida, id: number): readonly [number, number] {
const todos = p.mapa.nascedouros;
const longe = todos.filter(
([x, z]) => distanciaAoJogador(p, x, z) >= DISTANCIA_MINIMA_DO_NASCEDOURO,
);
const lista = longe.length > 0 ? longe : todos;
return lista[id % lista.length];
}
export function nascer(p: Partida): void {
const proximo = p.fila[0];
const aVista = p.zumbis.filter((z) => z.estado !== "morrendo").length;
if (
p.passo % INTERVALO_DE_NASCER !== 0 ||
!proximo ||
aVista >= p.tetoAVista
) {
return;
}
p.fila.shift();
const id = p.proximoId++;
const [x, z] = nascedouroPara(p, id);
p.zumbis.push({
id,
tipo: proximo.tipo,
problemaId: proximo.problemaId,
x,
z,
rumo: Math.atan2(p.jogador.x - x, p.jogador.z - z),
vida: ajustar(proximo.tipo, p.dificuldade).vida,
estado: "andando",
tempo: 0,
});
}
function pontuar(p: Partida, autor: Autor): void {
if (autor === "jogador") {
p.placar.jogador++;
return;
}
p.placar.drones++;
const drone = p.drones.find((d) => d.id === autor);
if (drone) drone.eliminacoes++;
}
function registrarMorte(p: Partida, zumbi: Zumbi, autor: Autor): void {
zumbi.estado = "morrendo";
zumbi.tempo = 0;
pontuar(p, autor);
p.eventos.push({ tipo: "morte", zumbiId: zumbi.id, autor });
if (sortear(p) < MUNICAO.chanceDeCaixa) {
p.caixas.push({ id: p.proximoId++, x: zumbi.x, z: zumbi.z });
}
}
/** Devolve true se este golpe matou. */
export function ferir(
p: Partida,
zumbi: Zumbi,
dano: number,
autor: Autor,
): boolean {
if (zumbi.estado === "morrendo") return false;
zumbi.vida -= dano;
if (zumbi.vida > 0) return false;
registrarMorte(p, zumbi, autor);
return true;
}
function noAlcance(p: Partida, zumbi: Zumbi): boolean {
const folga =
distanciaAoJogador(p, zumbi.x, zumbi.z) -
ZUMBIS[zumbi.tipo].raio -
JOGADOR.raio;
return folga <= ATAQUE_DO_ZUMBI.alcance;
}
function entrarEm(zumbi: Zumbi, estado: "andando" | "atacando"): void {
if (zumbi.estado === estado) return;
zumbi.estado = estado;
zumbi.tempo = 0;
}
function atacar(p: Partida, zumbi: Zumbi): void {
entrarEm(zumbi, "atacando");
zumbi.rumo = Math.atan2(p.jogador.x - zumbi.x, p.jogador.z - zumbi.z);
if (zumbi.tempo % ATAQUE_DO_ZUMBI.intervalo !== PASSO_DO_GOLPE) return;
const { dano } = ajustar(zumbi.tipo, p.dificuldade);
p.jogador.vida = Math.max(0, p.jogador.vida - dano);
p.eventos.push({ tipo: "golpe", dano, zumbiId: zumbi.id });
}
/** Em linha reta só se o corpo inteiro passa até o jogador; senão segue o campo. */
function direcaoDoZumbi(
p: Partida,
campo: Int16Array,
zumbi: Zumbi,
): [number, number] {
const { x, z } = zumbi;
const raio = raioNoMapa(zumbi);
if (caminhoLivre(p.mapa, x, z, p.jogador.x, p.jogador.z, raio)) {
const d = distanciaAoJogador(p, x, z);
return [(p.jogador.x - x) / d, (p.jogador.z - z) / d];
}
return direcaoNoCampo(p.mapa, campo, x, z);
}
function andar(p: Partida, campo: Int16Array, zumbi: Zumbi): void {
entrarEm(zumbi, "andando");
const [dx, dz] = direcaoDoZumbi(p, campo, zumbi);
if (dx === 0 && dz === 0) return;
const passo = ajustar(zumbi.tipo, p.dificuldade).velocidade * PASSO;
[zumbi.x, zumbi.z] = moverComColisao(
p.mapa,
zumbi.x,
zumbi.z,
dx * passo,
dz * passo,
raioNoMapa(zumbi),
);
zumbi.rumo = Math.atan2(dx, dz);
}
function avancarUm(p: Partida, campo: Int16Array, zumbi: Zumbi): void {
zumbi.tempo++;
if (zumbi.estado === "morrendo") return;
if (noAlcance(p, zumbi)) atacar(p, zumbi);
else andar(p, campo, zumbi);
}
/**
* Só o mais longe do jogador cede, recuando a sobreposição inteira; no mesmo ponto, no eixo x.
* Meio a meio, dois zumbis na boca de um vão de uma célula se empurravam contra as mesas para sempre.
*/
function separarPar(p: Partida, a: Zumbi, b: Zumbi): void {
const d = Math.hypot(b.x - a.x, b.z - a.z);
const sobra = ZUMBIS[a.tipo].raio + ZUMBIS[b.tipo].raio - d;
if (sobra <= 0) return;
const bCede =
distanciaAoJogador(p, a.x, a.z) <= distanciaAoJogador(p, b.x, b.z);
const [fica, cede] = bCede ? [a, b] : [b, a];
const [ux, uz] =
d === 0 ? [1, 0] : [(cede.x - fica.x) / d, (cede.z - fica.z) / d];
[cede.x, cede.z] = moverComColisao(
p.mapa,
cede.x,
cede.z,
ux * sobra,
uz * sobra,
raioNoMapa(cede),
);
}
function separar(p: Partida): void {
const vivos = p.zumbis.filter((z) => z.estado !== "morrendo");
for (let i = 0; i < vivos.length; i++) {
for (let j = i + 1; j < vivos.length; j++) {
separarPar(p, vivos[i], vivos[j]);
}
}
}
export function avancarZumbis(p: Partida, campo: Int16Array): void {
for (const zumbi of p.zumbis) avancarUm(p, campo, zumbi);
const restantes = p.zumbis.filter(
(z) => z.estado !== "morrendo" || z.tempo < ATAQUE_DO_ZUMBI.passosMorrendo,
);
p.zumbis.splice(0, p.zumbis.length, ...restantes);
separar(p);
}