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.
460 lines
13 KiB
TypeScript
460 lines
13 KiB
TypeScript
import { campoDeFluxo, lerMapa } from "./mapa";
|
|
import { sortear } from "./rng";
|
|
import type { Partida, Zumbi } from "./tipos";
|
|
import { ATAQUE_DO_ZUMBI, MUNICAO } from "./valores";
|
|
import { avancarZumbis, ferir, nascer } from "./zumbis";
|
|
|
|
const SALA = [
|
|
"##############",
|
|
"#J...........#",
|
|
"#.....N......#",
|
|
"##############",
|
|
];
|
|
|
|
const DOIS_NASCEDOUROS_LONGE = [
|
|
"################",
|
|
"#JN.........N.N#",
|
|
"#..............#",
|
|
"################",
|
|
];
|
|
|
|
const PAREDE_NO_MEIO = ["#######", "#J#..N#", "#.#...#", "#.....#", "#######"];
|
|
|
|
const BOLSO_FECHADO = ["#######", "#J#..N#", "#.#...#", "#######"];
|
|
|
|
/** Mesa entre o zumbi e o jogador: o tiro passa, o zumbi não. */
|
|
const MESA_NO_CAMINHO = [
|
|
"#########",
|
|
"#.......#",
|
|
"#J..M..N#",
|
|
"#.......#",
|
|
"#########",
|
|
];
|
|
|
|
/** Corredor com parede em cima de x 4 a 6; o jogador fica depois da curva, fora da vista. */
|
|
const QUINA = [
|
|
"#########",
|
|
"#...##..#",
|
|
"#......N#",
|
|
"#.#######",
|
|
"#J#######",
|
|
"#########",
|
|
];
|
|
|
|
/** Vão de uma célula entre duas mesas (x 4): cabe um zumbi por vez. */
|
|
const VAO_ENTRE_MESAS = [
|
|
"#########",
|
|
"#...M...#",
|
|
"#J.....N#",
|
|
"#...M...#",
|
|
"#########",
|
|
];
|
|
|
|
function partida(linhas: string[], extra: Partial<Partida> = {}): Partida {
|
|
const mapa = lerMapa(linhas);
|
|
return {
|
|
passo: 0,
|
|
rng: 1,
|
|
dificuldade: "normal",
|
|
problemaId: "p",
|
|
hordas: ["h"],
|
|
horda: 0,
|
|
fase: "horda",
|
|
tempoFase: 0,
|
|
tetoAVista: 16,
|
|
mapa,
|
|
jogador: {
|
|
x: mapa.inicio[0],
|
|
z: mapa.inicio[1],
|
|
rumo: 0,
|
|
inclinacao: 0,
|
|
vida: 100,
|
|
arma: 0,
|
|
municao: 90,
|
|
recarga: 0,
|
|
},
|
|
zumbis: [],
|
|
fila: [],
|
|
drones: [],
|
|
caixas: [],
|
|
feed: [],
|
|
resolvidos: [],
|
|
placar: { jogador: 0, drones: 0 },
|
|
eventos: [],
|
|
proximoId: 1,
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
function zumbi(extra: Partial<Zumbi> = {}): Zumbi {
|
|
return {
|
|
id: 1,
|
|
tipo: "comum",
|
|
problemaId: "p",
|
|
x: 8.5,
|
|
z: 1.5,
|
|
rumo: 0,
|
|
vida: 100,
|
|
estado: "andando",
|
|
tempo: 0,
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
function avancar(p: Partida): void {
|
|
avancarZumbis(p, campoDeFluxo(p.mapa, p.jogador.x, p.jogador.z));
|
|
}
|
|
|
|
/** Uma semente de `p.rng` cujo próximo sorteio fica abaixo (ou não) da chance de caixa. */
|
|
function sementeComSorteio(abaixo: boolean): number {
|
|
for (let s = 1; ; s++) {
|
|
if (sortear({ rng: s }) < MUNICAO.chanceDeCaixa === abaixo) return s;
|
|
}
|
|
}
|
|
|
|
describe("nascer", () => {
|
|
it("não age fora do passo múltiplo de 20", () => {
|
|
const p = partida(SALA, {
|
|
passo: 21,
|
|
fila: [{ tipo: "comum", problemaId: "a" }],
|
|
});
|
|
nascer(p);
|
|
expect(p.zumbis).toHaveLength(0);
|
|
expect(p.fila).toHaveLength(1);
|
|
});
|
|
|
|
it("não age com a fila vazia", () => {
|
|
const p = partida(SALA, { passo: 20 });
|
|
nascer(p);
|
|
expect(p.zumbis).toHaveLength(0);
|
|
expect(p.proximoId).toBe(1);
|
|
});
|
|
|
|
it("respeita o teto de zumbis à vista, sem contar os morrendo", () => {
|
|
const p = partida(SALA, {
|
|
passo: 20,
|
|
tetoAVista: 1,
|
|
zumbis: [zumbi({ id: 1 })],
|
|
fila: [{ tipo: "comum", problemaId: "a" }],
|
|
});
|
|
nascer(p);
|
|
expect(p.zumbis).toHaveLength(1);
|
|
p.zumbis[0].estado = "morrendo";
|
|
nascer(p);
|
|
expect(p.zumbis).toHaveLength(2);
|
|
expect(p.fila).toHaveLength(0);
|
|
});
|
|
|
|
it("cria o primeiro da fila com os valores ajustados, virado para o jogador", () => {
|
|
const p = partida(SALA, {
|
|
passo: 40,
|
|
dificuldade: "dificil",
|
|
proximoId: 5,
|
|
fila: [
|
|
{ tipo: "blindado", problemaId: "a" },
|
|
{ tipo: "comum", problemaId: "b" },
|
|
],
|
|
});
|
|
nascer(p);
|
|
expect(p.fila).toEqual([{ tipo: "comum", problemaId: "b" }]);
|
|
expect(p.proximoId).toBe(6);
|
|
const z = p.zumbis[0];
|
|
expect(z).toMatchObject({
|
|
id: 5,
|
|
tipo: "blindado",
|
|
problemaId: "a",
|
|
vida: 250,
|
|
estado: "andando",
|
|
tempo: 0,
|
|
});
|
|
expect([z.x, z.z]).toEqual([6.5, 2.5]);
|
|
expect(z.rumo).toBeCloseTo(Math.atan2(1.5 - 6.5, 1.5 - 2.5));
|
|
});
|
|
|
|
it("escolhe só nascedouros a 8 m ou mais, pelo índice id % candidatos", () => {
|
|
const fila = [{ tipo: "comum" as const, problemaId: "a" }];
|
|
const a = partida(DOIS_NASCEDOUROS_LONGE, {
|
|
passo: 0,
|
|
proximoId: 4,
|
|
fila: [...fila],
|
|
});
|
|
const b = partida(DOIS_NASCEDOUROS_LONGE, {
|
|
passo: 0,
|
|
proximoId: 5,
|
|
fila: [...fila],
|
|
});
|
|
nascer(a);
|
|
nascer(b);
|
|
expect(a.zumbis[0].x).toBe(12.5);
|
|
expect(b.zumbis[0].x).toBe(14.5);
|
|
});
|
|
|
|
it("usa todos os nascedouros quando nenhum está longe", () => {
|
|
const p = partida(["#####", "#JN.#", "#####"], {
|
|
fila: [{ tipo: "comum", problemaId: "a" }],
|
|
});
|
|
nascer(p);
|
|
expect(p.zumbis[0].x).toBe(2.5);
|
|
});
|
|
});
|
|
|
|
describe("ferir", () => {
|
|
it("ignora zumbi que já está morrendo", () => {
|
|
const p = partida(SALA);
|
|
const z = zumbi({ estado: "morrendo", vida: 0 });
|
|
expect(ferir(p, z, 50, "jogador")).toBe(false);
|
|
expect(z.vida).toBe(0);
|
|
expect(p.placar.jogador).toBe(0);
|
|
});
|
|
|
|
it("só desconta a vida quando não mata", () => {
|
|
const p = partida(SALA);
|
|
const z = zumbi();
|
|
expect(ferir(p, z, 30, "jogador")).toBe(false);
|
|
expect(z.vida).toBe(70);
|
|
expect(z.estado).toBe("andando");
|
|
expect(p.eventos).toEqual([]);
|
|
});
|
|
|
|
it("mata para o jogador: placar, evento e estado", () => {
|
|
const p = partida(SALA, { rng: sementeComSorteio(false), proximoId: 9 });
|
|
const z = zumbi({ id: 3, tempo: 12 });
|
|
expect(ferir(p, z, 100, "jogador")).toBe(true);
|
|
expect(z.estado).toBe("morrendo");
|
|
expect(z.tempo).toBe(0);
|
|
expect(p.placar).toEqual({ jogador: 1, drones: 0 });
|
|
expect(p.eventos).toEqual([
|
|
{ tipo: "morte", zumbiId: 3, autor: "jogador" },
|
|
]);
|
|
expect(p.caixas).toEqual([]);
|
|
expect(p.proximoId).toBe(9);
|
|
});
|
|
|
|
it("larga uma caixa quando o sorteio fica abaixo da chance", () => {
|
|
const p = partida(SALA, { rng: sementeComSorteio(true), proximoId: 9 });
|
|
ferir(p, zumbi({ x: 4.5, z: 2.5 }), 500, "jogador");
|
|
expect(p.caixas).toEqual([{ id: 9, x: 4.5, z: 2.5 }]);
|
|
expect(p.proximoId).toBe(10);
|
|
});
|
|
|
|
it("morte por drone soma em placar.drones e nas eliminações dele", () => {
|
|
const p = partida(SALA, {
|
|
drones: [
|
|
{
|
|
id: 7,
|
|
problemaId: "a",
|
|
x: 0,
|
|
z: 0,
|
|
rumo: 0,
|
|
recarga: 0,
|
|
eliminacoes: 0,
|
|
},
|
|
],
|
|
});
|
|
ferir(p, zumbi({ id: 2 }), 100, 7);
|
|
expect(p.placar).toEqual({ jogador: 0, drones: 1 });
|
|
expect(p.drones[0].eliminacoes).toBe(1);
|
|
expect(p.eventos).toEqual([{ tipo: "morte", zumbiId: 2, autor: 7 }]);
|
|
});
|
|
|
|
it("morte por drone que não existe só soma no placar", () => {
|
|
const p = partida(SALA);
|
|
ferir(p, zumbi(), 100, 99);
|
|
expect(p.placar.drones).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("avancarZumbis", () => {
|
|
it("remove o morrendo só ao completar a animação", () => {
|
|
const passos = ATAQUE_DO_ZUMBI.passosMorrendo;
|
|
const p = partida(SALA, {
|
|
zumbis: [
|
|
zumbi({ id: 1, estado: "morrendo", tempo: passos - 2 }),
|
|
zumbi({ id: 2, estado: "morrendo", tempo: passos - 1 }),
|
|
],
|
|
});
|
|
avancar(p);
|
|
expect(p.zumbis.map((z) => z.id)).toEqual([1]);
|
|
expect(p.zumbis[0].tempo).toBe(passos - 1);
|
|
});
|
|
|
|
it("anda em linha reta quando vê o jogador", () => {
|
|
const p = partida(SALA, { zumbis: [zumbi({ tempo: 3 })] });
|
|
avancar(p);
|
|
const z = p.zumbis[0];
|
|
expect(z.x).toBeCloseTo(8.5 - 1.2 / 60);
|
|
expect(z.z).toBeCloseTo(1.5);
|
|
expect(z.rumo).toBeCloseTo(-Math.PI / 2);
|
|
expect(z.estado).toBe("andando");
|
|
expect(z.tempo).toBe(4);
|
|
});
|
|
|
|
it("usa a velocidade ajustada pela dificuldade", () => {
|
|
const p = partida(SALA, {
|
|
dificuldade: "dificil",
|
|
zumbis: [zumbi()],
|
|
});
|
|
avancar(p);
|
|
expect(p.zumbis[0].x).toBeCloseTo(8.5 - (1.2 * 1.3) / 60);
|
|
});
|
|
|
|
it("zera o tempo ao voltar a andar", () => {
|
|
const p = partida(SALA, {
|
|
zumbis: [zumbi({ estado: "atacando", tempo: 40 })],
|
|
});
|
|
avancar(p);
|
|
expect(p.zumbis[0].estado).toBe("andando");
|
|
expect(p.zumbis[0].tempo).toBe(0);
|
|
});
|
|
|
|
it("sem caminho livre, contorna a parede pelo campo de fluxo", () => {
|
|
const p = partida(PAREDE_NO_MEIO, {
|
|
zumbis: [zumbi({ x: 3.5, z: 1.5 })],
|
|
});
|
|
avancar(p);
|
|
const z = p.zumbis[0];
|
|
expect(z.x).toBeCloseTo(3.5);
|
|
expect(z.z).toBeGreaterThan(1.5);
|
|
expect(z.rumo).toBeCloseTo(0);
|
|
});
|
|
|
|
it("sem caminho nem visão, fica parado e mantém o rumo", () => {
|
|
const p = partida(BOLSO_FECHADO, {
|
|
zumbis: [zumbi({ x: 4.5, z: 1.5, rumo: 1.25 })],
|
|
});
|
|
avancar(p);
|
|
const z = p.zumbis[0];
|
|
expect([z.x, z.z, z.rumo]).toEqual([4.5, 1.5, 1.25]);
|
|
});
|
|
|
|
it("com mesa entre ele e o jogador, contorna a mesa e chega", () => {
|
|
const p = partida(MESA_NO_CAMINHO, {
|
|
zumbis: [zumbi({ x: 7.5, z: 2.5 })],
|
|
});
|
|
for (let i = 0; i < 600; i++) avancar(p);
|
|
expect(p.zumbis[0].estado).toBe("atacando");
|
|
});
|
|
|
|
it("encostado numa quina, não fica parado e chega", () => {
|
|
const p = partida(QUINA, { zumbis: [zumbi({ x: 6.36, z: 2.0 })] });
|
|
avancar(p);
|
|
expect(p.zumbis[0].z).toBeGreaterThan(2.0);
|
|
for (let i = 0; i < 600; i++) avancar(p);
|
|
expect(p.zumbis[0].estado).toBe("atacando");
|
|
});
|
|
|
|
it("ataca ao chegar no alcance, virado para o jogador, sem golpear de cara", () => {
|
|
const p = partida(SALA, {
|
|
zumbis: [zumbi({ x: 2.5, z: 1.5, tempo: 7 })],
|
|
});
|
|
avancar(p);
|
|
const z = p.zumbis[0];
|
|
expect(z.estado).toBe("atacando");
|
|
expect(z.tempo).toBe(0);
|
|
expect(z.rumo).toBeCloseTo(-Math.PI / 2);
|
|
expect([z.x, z.z]).toEqual([2.5, 1.5]);
|
|
expect(p.eventos).toEqual([]);
|
|
});
|
|
|
|
it("golpeia só no passo certo do intervalo", () => {
|
|
const p = partida(SALA, {
|
|
dificuldade: "dificil",
|
|
zumbis: [zumbi({ x: 2.5, z: 1.5, estado: "atacando", tempo: 17 })],
|
|
});
|
|
avancar(p);
|
|
avancar(p);
|
|
expect(p.eventos).toEqual([]);
|
|
avancar(p);
|
|
expect(p.zumbis[0].tempo).toBe(20);
|
|
expect(p.eventos).toEqual([
|
|
{ tipo: "golpe", dano: 15, zumbiId: p.zumbis[0].id },
|
|
]);
|
|
expect(p.jogador.vida).toBe(85);
|
|
for (let i = 0; i < ATAQUE_DO_ZUMBI.intervalo - 1; i++) avancar(p);
|
|
expect(p.eventos).toHaveLength(1);
|
|
avancar(p);
|
|
expect(p.eventos).toHaveLength(2);
|
|
});
|
|
|
|
it("a vida do jogador não fica negativa", () => {
|
|
const p = partida(SALA, {
|
|
zumbis: [zumbi({ x: 2.5, z: 1.5, estado: "atacando", tempo: 19 })],
|
|
});
|
|
p.jogador.vida = 4;
|
|
avancar(p);
|
|
expect(p.jogador.vida).toBe(0);
|
|
});
|
|
|
|
it("dois zumbis sobrepostos terminam afastados", () => {
|
|
const p = partida(SALA, {
|
|
zumbis: [
|
|
zumbi({ id: 1, x: 6.5, z: 1.5 }),
|
|
zumbi({ id: 2, x: 6.8, z: 1.5 }),
|
|
],
|
|
});
|
|
avancar(p);
|
|
const [a, b] = p.zumbis;
|
|
expect(Math.hypot(b.x - a.x, b.z - a.z)).toBeGreaterThanOrEqual(0.7 - 1e-9);
|
|
});
|
|
|
|
it("na sobreposição só o mais longe do jogador recua; o da frente segue o passo normal", () => {
|
|
for (const perto of [0, 1]) {
|
|
const xs = perto === 0 ? [6.5, 6.8] : [6.8, 6.5];
|
|
const p = partida(SALA, {
|
|
zumbis: xs.map((x, i) => zumbi({ id: i + 1, x, z: 1.5 })),
|
|
});
|
|
avancar(p);
|
|
expect(p.zumbis[perto].x).toBeCloseTo(6.5 - 1.2 / 60);
|
|
expect(p.zumbis[1 - perto].x).toBeCloseTo(6.5 - 1.2 / 60 + 0.7);
|
|
}
|
|
});
|
|
|
|
it("par exatamente no mesmo ponto é afastado no eixo x", () => {
|
|
const p = partida(SALA, {
|
|
zumbis: [
|
|
zumbi({ id: 1, x: 6.5, z: 1.5 }),
|
|
zumbi({ id: 2, x: 6.5, z: 1.5 }),
|
|
],
|
|
});
|
|
avancar(p);
|
|
const [a, b] = p.zumbis;
|
|
expect(b.x - a.x).toBeCloseTo(0.7);
|
|
expect(b.z).toBeCloseTo(a.z);
|
|
});
|
|
|
|
it("dois zumbis lado a lado na boca de um vão estreito passam os dois, um por vez", () => {
|
|
const p = partida(VAO_ENTRE_MESAS, {
|
|
zumbis: [
|
|
zumbi({ id: 1, x: 5.4, z: 2.15 }),
|
|
zumbi({ id: 2, x: 5.4, z: 2.85 }),
|
|
],
|
|
});
|
|
for (let i = 0; i < 300; i++) avancar(p);
|
|
expect(p.zumbis.map((z) => z.x < 4)).toEqual([true, true]);
|
|
});
|
|
|
|
it("não separa quem está morrendo nem quem já está afastado", () => {
|
|
const p = partida(SALA, {
|
|
zumbis: [
|
|
zumbi({ id: 1, x: 6.5, z: 1.5, estado: "morrendo" }),
|
|
zumbi({ id: 2, x: 6.5, z: 1.5, estado: "morrendo" }),
|
|
zumbi({ id: 3, x: 9.5, z: 1.5 }),
|
|
zumbi({ id: 4, x: 11.5, z: 1.5 }),
|
|
],
|
|
});
|
|
avancar(p);
|
|
expect([p.zumbis[0].x, p.zumbis[1].x]).toEqual([6.5, 6.5]);
|
|
expect(p.zumbis[2].x).toBeCloseTo(9.5 - 1.2 / 60);
|
|
expect(p.zumbis[3].x).toBeCloseTo(11.5 - 1.2 / 60);
|
|
});
|
|
|
|
it("chefão no centro de uma célula cercada de parede por três lados anda para o lado livre", () => {
|
|
const p = partida(["#######", "#N...J#", "#######"], {
|
|
zumbis: [zumbi({ tipo: "chefao", x: 1.5, z: 1.5 })],
|
|
});
|
|
avancar(p);
|
|
expect(p.zumbis[0].x).toBeGreaterThan(1.5);
|
|
expect(p.zumbis[0].z).toBeCloseTo(1.5);
|
|
});
|
|
});
|