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.
227 lines
6.4 KiB
TypeScript
227 lines
6.4 KiB
TypeScript
import { avancar, criarPartida } from "./partida";
|
|
import { pilotar } from "./piloto";
|
|
import type { Partida, TipoZumbi, Zumbi } from "./tipos";
|
|
import { ESCRITORIO, ZUMBIS } from "./valores";
|
|
|
|
/** 21 x 15, parede por dentro em x = 10, de z = 5 a z = 9. */
|
|
const MAPA = Array.from({ length: 15 }, (_, z) => {
|
|
if (z === 0 || z === 14) return "#".repeat(21);
|
|
const miolo = Array.from({ length: 19 }, (_, i) => {
|
|
if (z === 1 && i === 0) return "N";
|
|
if (z === 2 && i === 0) return "J";
|
|
return i === 9 && z >= 5 && z <= 9 ? "#" : ".";
|
|
});
|
|
return `#${miolo.join("")}#`;
|
|
});
|
|
|
|
const JX = 5.5;
|
|
const JZ = 7.5;
|
|
|
|
function zumbi(
|
|
id: number,
|
|
x: number,
|
|
z: number,
|
|
tipo: TipoZumbi = "comum",
|
|
estado: Zumbi["estado"] = "andando",
|
|
): Zumbi {
|
|
return {
|
|
id,
|
|
tipo,
|
|
problemaId: "site-lento",
|
|
x,
|
|
z,
|
|
rumo: 0,
|
|
vida: ZUMBIS[tipo].vida,
|
|
estado,
|
|
tempo: 0,
|
|
};
|
|
}
|
|
|
|
/** Jogador em (5.5, 7.5), rumo 0, com os zumbis dados. */
|
|
function cena(...zumbis: Zumbi[]): Partida {
|
|
const p = criarPartida({
|
|
semente: 1,
|
|
dificuldade: "facil",
|
|
problemaId: "site-lento",
|
|
mapa: MAPA,
|
|
});
|
|
p.jogador.x = JX;
|
|
p.jogador.z = JZ;
|
|
p.zumbis = zumbis;
|
|
return p;
|
|
}
|
|
|
|
/** Zumbi a `distancia` metros do jogador, no rumo dado. */
|
|
function aoRumo(
|
|
id: number,
|
|
rumo: number,
|
|
distancia: number,
|
|
tipo: TipoZumbi = "comum",
|
|
): Zumbi {
|
|
return zumbi(
|
|
id,
|
|
JX + Math.sin(rumo) * distancia,
|
|
JZ + Math.cos(rumo) * distancia,
|
|
tipo,
|
|
);
|
|
}
|
|
|
|
describe("pilotar: sem alvo", () => {
|
|
it("fica parado sem zumbis", () => {
|
|
expect(pilotar(cena())).toEqual({
|
|
mover: [0, 0],
|
|
olhar: [0, 0],
|
|
atirar: false,
|
|
arma: 0,
|
|
});
|
|
});
|
|
|
|
it("ignora zumbis morrendo", () => {
|
|
const p = cena(zumbi(1, 5.5, 9.5, "comum", "morrendo"));
|
|
expect(pilotar(p).atirar).toBe(false);
|
|
expect(pilotar(p).mover).toEqual([0, 0]);
|
|
});
|
|
});
|
|
|
|
describe("pilotar: escolha do alvo", () => {
|
|
it("prefere o mais próximo com linha de visão", () => {
|
|
const atras = zumbi(1, 11.5, 7.5);
|
|
const livre = zumbi(2, 1.5, 1.5);
|
|
expect(pilotar(cena(atras, livre)).olhar[0]).toBe(-0.12);
|
|
});
|
|
|
|
it("sem visão de ninguém, escolhe o mais próximo", () => {
|
|
const perto = zumbi(1, 11.5, 7.5);
|
|
const longe = zumbi(2, 11.5, 5.5);
|
|
expect(pilotar(cena(longe, perto)).olhar[0]).toBe(0.12);
|
|
});
|
|
|
|
it("não atira sem linha de visão, mesmo alinhado", () => {
|
|
const p = cena(zumbi(1, 11.5, 7.5));
|
|
p.jogador.rumo = Math.PI / 2;
|
|
expect(pilotar(p).atirar).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("pilotar: mira", () => {
|
|
it("gira no máximo 0,12 rad por passo", () => {
|
|
expect(pilotar(cena(aoRumo(1, 1, 5))).olhar[0]).toBe(0.12);
|
|
expect(pilotar(cena(aoRumo(1, -1, 5))).olhar[0]).toBe(-0.12);
|
|
});
|
|
|
|
it("gira só o que falta quando o erro é menor que o limite", () => {
|
|
const p = cena(aoRumo(1, 0.03, 5));
|
|
expect(pilotar(p).olhar[0]).toBeCloseTo(0.03);
|
|
});
|
|
|
|
it("normaliza a diferença de rumo para −π a π", () => {
|
|
const p = cena(aoRumo(1, -3, 5));
|
|
p.jogador.rumo = 3;
|
|
expect(pilotar(p).olhar[0]).toBe(0.12);
|
|
});
|
|
|
|
it("inclina no máximo 0,05 rad por passo, para baixo e para cima", () => {
|
|
const p = cena(aoRumo(1, 0, 5));
|
|
expect(pilotar(p).olhar[1]).toBe(-0.05);
|
|
p.jogador.inclinacao = -1;
|
|
expect(pilotar(p).olhar[1]).toBe(0.05);
|
|
});
|
|
|
|
it("mira a 65% da altura do tipo", () => {
|
|
const p = cena(aoRumo(1, 0, 5));
|
|
p.jogador.inclinacao = Math.atan2(0.65 * ZUMBIS.comum.altura - 1.6, 5);
|
|
expect(pilotar(p).olhar[1]).toBeCloseTo(0);
|
|
});
|
|
|
|
it("atira quando o erro de rumo é menor que 0,05 e há visão", () => {
|
|
expect(pilotar(cena(aoRumo(1, 0.04, 5))).atirar).toBe(true);
|
|
});
|
|
|
|
it("não atira com erro de rumo de 0,05 ou mais", () => {
|
|
expect(pilotar(cena(aoRumo(1, 0.06, 5))).atirar).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("pilotar: arma", () => {
|
|
it("usa a faca quando a borda está a 1,3 m ou menos", () => {
|
|
const comum = ZUMBIS.comum.raio;
|
|
expect(pilotar(cena(aoRumo(1, 0, 1.25 + comum))).arma).toBe(2);
|
|
expect(pilotar(cena(aoRumo(1, 0, 1.4 + comum))).arma).toBe(0);
|
|
});
|
|
|
|
it("usa a automática com 3 zumbis a até 12 m", () => {
|
|
const tres = cena(aoRumo(1, 0, 5), aoRumo(2, 1, 6), aoRumo(3, 2, 12));
|
|
expect(pilotar(tres).arma).toBe(1);
|
|
});
|
|
|
|
it("fica na pistola com só 2 zumbis a até 12 m", () => {
|
|
const dois = cena(aoRumo(1, 0, 5), aoRumo(2, 1, 6), aoRumo(3, 2, 12.5));
|
|
expect(pilotar(dois).arma).toBe(0);
|
|
});
|
|
|
|
it("usa a automática contra blindado e contra chefão", () => {
|
|
expect(pilotar(cena(aoRumo(1, 0, 6, "blindado"))).arma).toBe(1);
|
|
expect(pilotar(cena(aoRumo(1, 0, 6, "chefao"))).arma).toBe(1);
|
|
});
|
|
|
|
it("fica na pistola contra um comum ou um rápido", () => {
|
|
expect(pilotar(cena(aoRumo(1, 0, 6, "rapido"))).arma).toBe(0);
|
|
});
|
|
|
|
it("sem bala não usa a automática", () => {
|
|
const p = cena(aoRumo(1, 0, 6, "blindado"));
|
|
p.jogador.municao = 0;
|
|
expect(pilotar(p).arma).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("pilotar: distância", () => {
|
|
it("anda para trás com o alvo a menos de 4 m", () => {
|
|
expect(pilotar(cena(aoRumo(1, 0, 3.9))).mover).toEqual([0, -1]);
|
|
});
|
|
|
|
it("fica parado com o alvo a 4 m ou mais", () => {
|
|
expect(pilotar(cena(aoRumo(1, 0, 4))).mover).toEqual([0, 0]);
|
|
});
|
|
});
|
|
|
|
describe("pilotar: partida inteira no escritório", () => {
|
|
function jogar(dificuldade: Partida["dificuldade"] = "facil") {
|
|
const p = criarPartida({
|
|
semente: 1,
|
|
dificuldade,
|
|
problemaId: "site-lento",
|
|
mapa: ESCRITORIO,
|
|
});
|
|
while (p.fase !== "vitoria" && p.fase !== "derrota" && p.passo < 36000) {
|
|
avancar(p, pilotar(p));
|
|
}
|
|
return p;
|
|
}
|
|
|
|
it("vence em menos de 36.000 passos, com 3 drones e 4 problemas resolvidos", () => {
|
|
const p = jogar();
|
|
expect(p.fase).toBe("vitoria");
|
|
expect(p.passo).toBeLessThan(36000);
|
|
expect(p.drones).toHaveLength(3);
|
|
expect(p.resolvidos).toHaveLength(4);
|
|
expect(p.placar.drones).toBeGreaterThan(0);
|
|
});
|
|
|
|
it.each(["normal", "dificil"] as const)(
|
|
"termina em vitória ou derrota antes de 36.000 passos no %s",
|
|
(dificuldade) => {
|
|
const p = jogar(dificuldade);
|
|
expect(["vitoria", "derrota"]).toContain(p.fase);
|
|
expect(p.passo).toBeLessThan(36000);
|
|
},
|
|
);
|
|
|
|
it("é determinística: mesma partida, mesmo passo e mesmo placar", () => {
|
|
const a = jogar();
|
|
const b = jogar();
|
|
expect(b.passo).toBe(a.passo);
|
|
expect(b.placar).toEqual(a.placar);
|
|
});
|
|
});
|