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,379 @@
|
||||
import { criarDrone } from "./drones";
|
||||
import { montarFila } from "./hordas";
|
||||
import * as mapaModulo from "./mapa";
|
||||
import { avancar, criarPartida } from "./partida";
|
||||
import { pilotar } from "./piloto";
|
||||
import { semear } from "./rng";
|
||||
import type { Dificuldade, Entrada, Fase, Partida } from "./tipos";
|
||||
import { ESCRITORIO, HORDAS, JOGADOR, MUNICAO, PASSO } from "./valores";
|
||||
|
||||
const MAPA = ["#########", "#N......#", "#.......#", "#......J#", "#########"];
|
||||
|
||||
const PARADO: Entrada = {
|
||||
mover: [0, 0],
|
||||
olhar: [0, 0],
|
||||
atirar: false,
|
||||
arma: 0,
|
||||
};
|
||||
|
||||
function nova(extra: Partial<Parameters<typeof criarPartida>[0]> = {}) {
|
||||
return criarPartida({
|
||||
semente: 1,
|
||||
dificuldade: "facil",
|
||||
problemaId: "site-lento",
|
||||
tetoAVista: 2,
|
||||
mapa: MAPA,
|
||||
...extra,
|
||||
});
|
||||
}
|
||||
|
||||
function passo(p: Partida, parcial: Partial<Entrada> = {}) {
|
||||
avancar(p, { ...PARADO, ...parcial });
|
||||
}
|
||||
|
||||
function em(p: Partida, fase: Fase, extra: Partial<Partida> = {}) {
|
||||
p.fase = fase;
|
||||
p.tempoFase = 0;
|
||||
p.jogador.x = 4.5;
|
||||
p.jogador.z = 2.5;
|
||||
Object.assign(p, extra);
|
||||
}
|
||||
|
||||
const FILA_UM = [{ tipo: "comum" as const, problemaId: "site-lento" }];
|
||||
|
||||
describe("criarPartida", () => {
|
||||
it("monta o estado inicial no mapa do escritório por padrão", () => {
|
||||
const p = criarPartida({
|
||||
semente: 7,
|
||||
dificuldade: "normal",
|
||||
problemaId: "site-lento",
|
||||
});
|
||||
expect(p.fase).toBe("inicio");
|
||||
expect(p.tetoAVista).toBe(16);
|
||||
expect(p.mapa.largura).toBe(ESCRITORIO[0].length);
|
||||
expect(p.jogador).toEqual({
|
||||
x: p.mapa.inicio[0],
|
||||
z: p.mapa.inicio[1],
|
||||
rumo: 0,
|
||||
inclinacao: 0,
|
||||
vida: JOGADOR.vida,
|
||||
arma: 0,
|
||||
municao: MUNICAO.inicial,
|
||||
recarga: 0,
|
||||
});
|
||||
expect(p.hordas).toHaveLength(4);
|
||||
expect(p.hordas[3]).toBe("site-lento");
|
||||
expect(p).toMatchObject({
|
||||
passo: 0,
|
||||
horda: 0,
|
||||
tempoFase: 0,
|
||||
zumbis: [],
|
||||
fila: [],
|
||||
drones: [],
|
||||
caixas: [],
|
||||
feed: [],
|
||||
resolvidos: [],
|
||||
placar: { jogador: 0, drones: 0 },
|
||||
eventos: [],
|
||||
proximoId: 1,
|
||||
problemaId: "site-lento",
|
||||
dificuldade: "normal",
|
||||
});
|
||||
});
|
||||
|
||||
it("usa o mapa e o teto informados e consome o sorteio da própria partida", () => {
|
||||
const p = nova({ tetoAVista: 3 });
|
||||
expect(p.tetoAVista).toBe(3);
|
||||
expect(p.mapa.largura).toBe(9);
|
||||
expect(p.jogador.x).toBe(7.5);
|
||||
expect(p.rng).not.toBe(semear(1));
|
||||
expect(nova().hordas).toEqual(p.hordas);
|
||||
});
|
||||
});
|
||||
|
||||
describe("avancar: jogador", () => {
|
||||
it("anda para a frente em +z com rumo 0", () => {
|
||||
const p = nova();
|
||||
em(p, "pausa");
|
||||
passo(p, { mover: [0, 1] });
|
||||
expect(p.jogador.x).toBeCloseTo(4.5);
|
||||
expect(p.jogador.z).toBeCloseTo(2.5 + JOGADOR.velocidade * PASSO);
|
||||
});
|
||||
|
||||
it("anda para a direita em −x com rumo 0", () => {
|
||||
const p = nova();
|
||||
em(p, "pausa");
|
||||
passo(p, { mover: [1, 0] });
|
||||
expect(p.jogador.x).toBeCloseTo(4.5 - JOGADOR.velocidade * PASSO);
|
||||
expect(p.jogador.z).toBeCloseTo(2.5);
|
||||
});
|
||||
|
||||
it("gira o rumo e anda para a frente na direção nova", () => {
|
||||
const p = nova();
|
||||
em(p, "pausa");
|
||||
passo(p, { olhar: [Math.PI / 2, 0], mover: [0, 1] });
|
||||
expect(p.jogador.rumo).toBeCloseTo(Math.PI / 2);
|
||||
expect(p.jogador.x).toBeCloseTo(4.5 + JOGADOR.velocidade * PASSO);
|
||||
expect(p.jogador.z).toBeCloseTo(2.5);
|
||||
});
|
||||
|
||||
it("limita o movimento diagonal a comprimento 1", () => {
|
||||
const p = nova();
|
||||
em(p, "pausa");
|
||||
passo(p, { mover: [1, 1] });
|
||||
const andou = Math.hypot(p.jogador.x - 4.5, p.jogador.z - 2.5);
|
||||
expect(andou).toBeCloseTo(JOGADOR.velocidade * PASSO);
|
||||
});
|
||||
|
||||
it("não aumenta um movimento menor que 1", () => {
|
||||
const p = nova();
|
||||
em(p, "pausa");
|
||||
passo(p, { mover: [0, 0.5] });
|
||||
expect(p.jogador.z).toBeCloseTo(2.5 + 0.5 * JOGADOR.velocidade * PASSO);
|
||||
});
|
||||
|
||||
it("soma a inclinação e a limita para cima e para baixo", () => {
|
||||
const p = nova();
|
||||
em(p, "pausa");
|
||||
passo(p, { olhar: [0, 0.1] });
|
||||
expect(p.jogador.inclinacao).toBeCloseTo(0.1);
|
||||
passo(p, { olhar: [0, 5] });
|
||||
expect(p.jogador.inclinacao).toBe(JOGADOR.inclinacaoMax);
|
||||
passo(p, { olhar: [0, -9] });
|
||||
expect(p.jogador.inclinacao).toBe(-JOGADOR.inclinacaoMax);
|
||||
});
|
||||
|
||||
it("troca de arma, mas só atira se pedido", () => {
|
||||
const p = nova();
|
||||
em(p, "pausa");
|
||||
passo(p, { arma: 1 });
|
||||
expect(p.jogador.arma).toBe(1);
|
||||
expect(p.eventos).toEqual([]);
|
||||
p.jogador.recarga = 0;
|
||||
passo(p, { arma: 1, atirar: true });
|
||||
expect(p.eventos[0]).toMatchObject({ tipo: "tiro", autor: "jogador" });
|
||||
});
|
||||
|
||||
it("recolhe as caixas ao alcance", () => {
|
||||
const p = nova();
|
||||
em(p, "pausa", { caixas: [{ id: 99, x: 4.5, z: 2.5 }] });
|
||||
passo(p);
|
||||
expect(p.caixas).toEqual([]);
|
||||
expect(p.jogador.municao).toBe(MUNICAO.inicial + MUNICAO.porCaixa);
|
||||
expect(p.eventos).toEqual([{ tipo: "caixa", balas: MUNICAO.porCaixa }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("avancar: fases", () => {
|
||||
it("esvazia os eventos e conta passos", () => {
|
||||
const p = nova();
|
||||
p.eventos.push({ tipo: "golpe", dano: 1, zumbiId: 0 });
|
||||
passo(p);
|
||||
expect(p.eventos).toEqual([]);
|
||||
expect(p.passo).toBe(1);
|
||||
expect(p.tempoFase).toBe(1);
|
||||
});
|
||||
|
||||
it("fica em inicio até o preparo acabar", () => {
|
||||
const p = nova();
|
||||
p.tempoFase = HORDAS.preparo - 2;
|
||||
passo(p);
|
||||
expect(p.fase).toBe("inicio");
|
||||
expect(p.eventos).toEqual([]);
|
||||
});
|
||||
|
||||
it("começa a horda 0 ao fim do preparo", () => {
|
||||
const p = nova();
|
||||
p.tempoFase = HORDAS.preparo - 1;
|
||||
passo(p);
|
||||
expect(p.fase).toBe("horda");
|
||||
expect(p.tempoFase).toBe(0);
|
||||
expect(p.horda).toBe(0);
|
||||
expect(p.fila).toEqual(montarFila(0, p.hordas[0], "facil"));
|
||||
expect(p.eventos).toEqual([{ tipo: "fase", fase: "horda" }]);
|
||||
});
|
||||
|
||||
it("não termina a horda com zumbis na fila", () => {
|
||||
const p = nova();
|
||||
em(p, "horda", { fila: [...FILA_UM], tetoAVista: 0 });
|
||||
passo(p);
|
||||
expect(p.fase).toBe("horda");
|
||||
expect(p.resolvidos).toEqual([]);
|
||||
});
|
||||
|
||||
it("não termina a horda com zumbis no mapa", () => {
|
||||
const p = nova();
|
||||
em(p, "horda", {
|
||||
zumbis: [
|
||||
{
|
||||
id: 50,
|
||||
tipo: "comum",
|
||||
problemaId: "site-lento",
|
||||
x: 1.5,
|
||||
z: 1.5,
|
||||
rumo: 0,
|
||||
vida: 100,
|
||||
estado: "andando",
|
||||
tempo: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
passo(p);
|
||||
expect(p.fase).toBe("horda");
|
||||
expect(p.resolvidos).toEqual([]);
|
||||
});
|
||||
|
||||
it("termina a horda: resolve, cria o drone, cura e vai para a pausa", () => {
|
||||
const p = nova();
|
||||
em(p, "horda", { horda: 1 });
|
||||
p.jogador.vida = 50;
|
||||
passo(p);
|
||||
const resolvido = p.hordas[1];
|
||||
expect(p.resolvidos).toEqual([resolvido]);
|
||||
expect(p.drones).toHaveLength(1);
|
||||
expect(p.drones[0].problemaId).toBe(resolvido);
|
||||
expect(p.jogador.vida).toBe(50 + JOGADOR.curaPorHorda);
|
||||
expect(p.fase).toBe("pausa");
|
||||
expect(p.tempoFase).toBe(0);
|
||||
expect(p.feed[0].texto).toMatch(/^Resolvido: /);
|
||||
expect(p.eventos).toEqual([
|
||||
{ tipo: "resolvido", problemaId: resolvido, droneId: p.drones[0].id },
|
||||
{ tipo: "fase", fase: "pausa" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("a cura não passa da vida máxima", () => {
|
||||
const p = nova();
|
||||
em(p, "horda");
|
||||
p.jogador.vida = 90;
|
||||
passo(p);
|
||||
expect(p.jogador.vida).toBe(JOGADOR.vida);
|
||||
});
|
||||
|
||||
it("vai para a derrota com a vida zerada, antes de resolver a horda", () => {
|
||||
const p = nova();
|
||||
em(p, "horda");
|
||||
p.jogador.vida = 0;
|
||||
passo(p);
|
||||
expect(p.fase).toBe("derrota");
|
||||
expect(p.resolvidos).toEqual([]);
|
||||
expect(p.drones).toEqual([]);
|
||||
expect(p.eventos).toEqual([{ tipo: "fase", fase: "derrota" }]);
|
||||
});
|
||||
|
||||
it("fica na pausa até o tempo acabar, e os drones seguem o jogador", () => {
|
||||
const p = nova();
|
||||
em(p, "pausa", { horda: 0 });
|
||||
const drone = criarDrone(p, "site-lento");
|
||||
drone.x = 1.5;
|
||||
drone.z = 1.5;
|
||||
passo(p);
|
||||
expect(p.fase).toBe("pausa");
|
||||
expect(p.horda).toBe(0);
|
||||
expect(drone.x).toBeGreaterThan(1.5);
|
||||
expect(p.eventos).toEqual([]);
|
||||
});
|
||||
|
||||
it("sai da pausa para a horda seguinte", () => {
|
||||
const p = nova();
|
||||
em(p, "pausa", { horda: 0 });
|
||||
p.tempoFase = HORDAS.pausa - 1;
|
||||
passo(p);
|
||||
expect(p.horda).toBe(1);
|
||||
expect(p.fase).toBe("horda");
|
||||
expect(p.tempoFase).toBe(0);
|
||||
expect(p.fila).toEqual(montarFila(1, p.hordas[1], "facil"));
|
||||
expect(p.eventos).toEqual([{ tipo: "fase", fase: "horda" }]);
|
||||
});
|
||||
|
||||
it("sai da pausa da horda 2 para o chefão", () => {
|
||||
const p = nova();
|
||||
em(p, "pausa", { horda: 2 });
|
||||
p.tempoFase = HORDAS.pausa - 1;
|
||||
passo(p);
|
||||
expect(p.horda).toBe(3);
|
||||
expect(p.fase).toBe("chefao");
|
||||
expect(p.fila).toEqual(montarFila(3, p.hordas[3], "facil"));
|
||||
expect(p.eventos).toEqual([{ tipo: "fase", fase: "chefao" }]);
|
||||
});
|
||||
|
||||
it("vence ao derrotar o chefão, sem criar drone", () => {
|
||||
const p = nova();
|
||||
em(p, "chefao", { horda: 3 });
|
||||
passo(p);
|
||||
expect(p.fase).toBe("vitoria");
|
||||
expect(p.resolvidos).toEqual(["site-lento"]);
|
||||
expect(p.drones).toEqual([]);
|
||||
expect(p.eventos).toEqual([
|
||||
{ tipo: "resolvido", problemaId: "site-lento", droneId: -1 },
|
||||
{ tipo: "fase", fase: "vitoria" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("perde no chefão com a vida zerada", () => {
|
||||
const p = nova();
|
||||
em(p, "chefao", { horda: 3 });
|
||||
p.jogador.vida = -5;
|
||||
passo(p);
|
||||
expect(p.fase).toBe("derrota");
|
||||
});
|
||||
|
||||
it.each(["vitoria", "derrota"] as const)(
|
||||
"depois da %s nada mais muda, só os eventos esvaziam",
|
||||
(fase) => {
|
||||
const p = nova();
|
||||
em(p, fase);
|
||||
p.eventos.push({ tipo: "golpe", dano: 1, zumbiId: 0 });
|
||||
const antes = JSON.stringify({ ...p, eventos: [] });
|
||||
passo(p, { mover: [1, 1], olhar: [1, 1], atirar: true, arma: 1 });
|
||||
expect(p.eventos).toEqual([]);
|
||||
expect(JSON.stringify(p)).toBe(antes);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("avancar: campo de fluxo", () => {
|
||||
it("só recalcula quando o jogador muda de célula", () => {
|
||||
const espiao = jest.spyOn(mapaModulo, "campoDeFluxo");
|
||||
const p = nova();
|
||||
em(p, "horda", { fila: [...FILA_UM], tetoAVista: 0 });
|
||||
passo(p);
|
||||
passo(p);
|
||||
expect(espiao).toHaveBeenCalledTimes(1);
|
||||
p.jogador.x -= 1;
|
||||
passo(p);
|
||||
expect(espiao).toHaveBeenCalledTimes(2);
|
||||
passo(p);
|
||||
expect(espiao).toHaveBeenCalledTimes(2);
|
||||
espiao.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("partida inteira com o piloto", () => {
|
||||
const TETO_DE_PASSOS = 60_000;
|
||||
|
||||
// regressão: na fácil, as sementes 4, 6, 13, 20, 23, 24 e 38 empacavam na horda 3 com "faltam 2"
|
||||
it.each<Dificuldade>(["facil", "normal", "dificil"])(
|
||||
"40 sementes na %s terminam em vitória ou derrota",
|
||||
(dificuldade) => {
|
||||
const fins: [number, Fase][] = [];
|
||||
for (let semente = 1; semente <= 40; semente++) {
|
||||
const p = criarPartida({
|
||||
semente,
|
||||
dificuldade,
|
||||
problemaId: "site-lento",
|
||||
});
|
||||
for (
|
||||
let i = 0;
|
||||
i < TETO_DE_PASSOS && p.fase !== "vitoria" && p.fase !== "derrota";
|
||||
i++
|
||||
) {
|
||||
avancar(p, pilotar(p));
|
||||
}
|
||||
fins.push([semente, p.fase]);
|
||||
}
|
||||
const presas = fins.filter(([, f]) => f !== "vitoria" && f !== "derrota");
|
||||
expect(presas).toEqual([]);
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user