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.
This commit is contained in:
1 parent
c7d09d63d4
commit
67b32fcab3
118 files changed
+16730
No files matched your search
@@ -0,0 +1,199 @@
|
||||
/** 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);
|
||||
}
|
||||
Reference in new issue
Block a user