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