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.
165 lines
4.5 KiB
TypeScript
165 lines
4.5 KiB
TypeScript
/** Integra a lógica do jogo: cria a partida e avança um passo por vez. Sem three, React nem DOM. */
|
|
|
|
import { atirar, recolherCaixas, resfriar, trocarArma } from "./armas";
|
|
import { avancarDrones, criarDrone } from "./drones";
|
|
import { registrarResolvido } from "./feed";
|
|
import { escolherHordas, montarFila } from "./hordas";
|
|
import { campoDeFluxo, lerMapa, moverComColisao } from "./mapa";
|
|
import { semear } from "./rng";
|
|
import type { Dificuldade, Entrada, Fase, Partida } from "./tipos";
|
|
import {
|
|
ESCRITORIO,
|
|
HORDAS,
|
|
JOGADOR,
|
|
MUNICAO,
|
|
PASSO,
|
|
TETO_A_VISTA,
|
|
} from "./valores";
|
|
import { avancarZumbis, nascer } from "./zumbis";
|
|
|
|
const HORDA_DO_CHEFAO = 3;
|
|
|
|
export function criarPartida(opcoes: {
|
|
semente: number;
|
|
dificuldade: Dificuldade;
|
|
problemaId: string;
|
|
tetoAVista?: number;
|
|
mapa?: readonly string[];
|
|
}): Partida {
|
|
const sorteio = { rng: semear(opcoes.semente) };
|
|
const hordas = escolherHordas(sorteio, opcoes.problemaId);
|
|
const mapa = lerMapa(opcoes.mapa ?? ESCRITORIO);
|
|
return {
|
|
passo: 0,
|
|
rng: sorteio.rng,
|
|
dificuldade: opcoes.dificuldade,
|
|
problemaId: opcoes.problemaId,
|
|
hordas,
|
|
horda: 0,
|
|
fase: "inicio",
|
|
tempoFase: 0,
|
|
tetoAVista: opcoes.tetoAVista ?? TETO_A_VISTA,
|
|
mapa,
|
|
jogador: {
|
|
x: mapa.inicio[0],
|
|
z: mapa.inicio[1],
|
|
rumo: 0,
|
|
inclinacao: 0,
|
|
vida: JOGADOR.vida,
|
|
arma: 0,
|
|
municao: MUNICAO.inicial,
|
|
recarga: 0,
|
|
},
|
|
zumbis: [],
|
|
fila: [],
|
|
drones: [],
|
|
caixas: [],
|
|
feed: [],
|
|
resolvidos: [],
|
|
placar: { jogador: 0, drones: 0 },
|
|
eventos: [],
|
|
proximoId: 1,
|
|
};
|
|
}
|
|
|
|
const campos = new WeakMap<Partida, { celula: number; campo: Int16Array }>();
|
|
|
|
/** Só recalcula o campo de fluxo quando o jogador muda de célula. */
|
|
function campoAtual(p: Partida): Int16Array {
|
|
const { x, z } = p.jogador;
|
|
const celula = Math.floor(z) * p.mapa.largura + Math.floor(x);
|
|
const guardado = campos.get(p);
|
|
if (guardado?.celula === celula) return guardado.campo;
|
|
const campo = campoDeFluxo(p.mapa, x, z);
|
|
campos.set(p, { celula, campo });
|
|
return campo;
|
|
}
|
|
|
|
function moverJogador(p: Partida, entrada: Entrada): void {
|
|
const j = p.jogador;
|
|
j.rumo += entrada.olhar[0];
|
|
j.inclinacao = Math.min(
|
|
JOGADOR.inclinacaoMax,
|
|
Math.max(-JOGADOR.inclinacaoMax, j.inclinacao + entrada.olhar[1]),
|
|
);
|
|
const [lado, frente] = entrada.mover;
|
|
const escala = Math.min(1, 1 / Math.hypot(lado, frente));
|
|
const passo = escala * JOGADOR.velocidade * PASSO;
|
|
const dx = (Math.sin(j.rumo) * frente - Math.cos(j.rumo) * lado) * passo;
|
|
const dz = (Math.cos(j.rumo) * frente + Math.sin(j.rumo) * lado) * passo;
|
|
[j.x, j.z] = moverComColisao(p.mapa, j.x, j.z, dx, dz, JOGADOR.raio);
|
|
}
|
|
|
|
function usarArma(p: Partida, entrada: Entrada): void {
|
|
trocarArma(p, entrada.arma);
|
|
resfriar(p);
|
|
if (entrada.atirar) atirar(p);
|
|
recolherCaixas(p);
|
|
}
|
|
|
|
function mudarFase(p: Partida, fase: Fase): void {
|
|
p.fase = fase;
|
|
p.eventos.push({ tipo: "fase", fase });
|
|
}
|
|
|
|
/** Começa a horda `p.horda`: a última é a do chefão. */
|
|
function comecarHorda(p: Partida): void {
|
|
p.tempoFase = 0;
|
|
p.fila = montarFila(p.horda, p.hordas[p.horda], p.dificuldade);
|
|
mudarFase(p, p.horda === HORDA_DO_CHEFAO ? "chefao" : "horda");
|
|
}
|
|
|
|
function fecharHorda(p: Partida): void {
|
|
const problemaId = p.hordas[p.horda];
|
|
p.resolvidos.push(problemaId);
|
|
registrarResolvido(p, problemaId);
|
|
if (p.fase === "chefao") {
|
|
p.eventos.push({ tipo: "resolvido", problemaId, droneId: -1 });
|
|
mudarFase(p, "vitoria");
|
|
return;
|
|
}
|
|
const drone = criarDrone(p, problemaId);
|
|
p.eventos.push({ tipo: "resolvido", problemaId, droneId: drone.id });
|
|
p.jogador.vida = Math.min(
|
|
JOGADOR.vida,
|
|
p.jogador.vida + JOGADOR.curaPorHorda,
|
|
);
|
|
p.tempoFase = 0;
|
|
mudarFase(p, "pausa");
|
|
}
|
|
|
|
function combater(p: Partida): void {
|
|
nascer(p);
|
|
avancarZumbis(p, campoAtual(p));
|
|
avancarDrones(p);
|
|
if (p.jogador.vida <= 0) {
|
|
mudarFase(p, "derrota");
|
|
} else if (p.fila.length === 0 && p.zumbis.length === 0) {
|
|
fecharHorda(p);
|
|
}
|
|
}
|
|
|
|
function avancarFase(p: Partida): void {
|
|
if (p.fase === "inicio") {
|
|
if (p.tempoFase >= HORDAS.preparo) comecarHorda(p);
|
|
} else if (p.fase === "pausa") {
|
|
avancarDrones(p);
|
|
if (p.tempoFase >= HORDAS.pausa) {
|
|
p.horda++;
|
|
comecarHorda(p);
|
|
}
|
|
} else {
|
|
combater(p);
|
|
}
|
|
}
|
|
|
|
export function avancar(p: Partida, entrada: Entrada): void {
|
|
p.eventos.length = 0;
|
|
if (p.fase === "vitoria" || p.fase === "derrota") return;
|
|
p.passo++;
|
|
p.tempoFase++;
|
|
moverJogador(p, entrada);
|
|
usarArma(p, entrada);
|
|
avancarFase(p);
|
|
}
|