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.
380 lines
11 KiB
TypeScript
380 lines
11 KiB
TypeScript
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([]);
|
||
},
|
||
);
|
||
});
|