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.
200 lines
5.8 KiB
TypeScript
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);
|
|
}
|