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 { 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 { 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); }); });