feat(jogo): expediente dos mortos, jogo de zumbis em primeira pessoa
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.
This commit is contained in:
1 parent
c7d09d63d4
commit
67b32fcab3
118 files changed
+16730
No files matched your search
@@ -0,0 +1,206 @@
|
||||
/** Armas do jogador: tiro por raio, faca, troca, recarga e coleta de caixas. Sem three, React nem DOM. */
|
||||
|
||||
import { raioAteParede } from "./mapa";
|
||||
import type { ArmaId, Partida, Zumbi } from "./tipos";
|
||||
import {
|
||||
ARCO_DA_FACA,
|
||||
ARMAS,
|
||||
FRACAO_DA_CABECA,
|
||||
JOGADOR,
|
||||
MULTIPLICADOR_DA_CABECA,
|
||||
MUNICAO,
|
||||
ZUMBIS,
|
||||
} from "./valores";
|
||||
import { ferir } from "./zumbis";
|
||||
|
||||
const FACA: ArmaId = 2;
|
||||
const RECARGA_DA_TROCA = 10;
|
||||
|
||||
/** Distância ao longo do raio até o círculo; `null` se não cruza ou fica atrás; 0 se a origem está dentro. */
|
||||
export function raioNoCirculo(
|
||||
ox: number,
|
||||
oz: number,
|
||||
dx: number,
|
||||
dz: number,
|
||||
cx: number,
|
||||
cz: number,
|
||||
r: number,
|
||||
): number | null {
|
||||
const fx = ox - cx;
|
||||
const fz = oz - cz;
|
||||
const fora = fx * fx + fz * fz - r * r;
|
||||
if (fora <= 0) return 0;
|
||||
const b = fx * dx + fz * dz;
|
||||
const delta = b * b - fora;
|
||||
if (delta < 0) return null;
|
||||
const t = -b - Math.sqrt(delta);
|
||||
return t < 0 ? null : t;
|
||||
}
|
||||
|
||||
export function trocarArma(p: Partida, arma: ArmaId): void {
|
||||
if (arma === p.jogador.arma) return;
|
||||
p.jogador.arma = arma;
|
||||
p.jogador.recarga = RECARGA_DA_TROCA;
|
||||
}
|
||||
|
||||
export function resfriar(p: Partida): void {
|
||||
p.jogador.recarga = Math.max(0, p.jogador.recarga - 1);
|
||||
}
|
||||
|
||||
export function recolherCaixas(p: Partida): void {
|
||||
const { x, z } = p.jogador;
|
||||
const perto = (c: { x: number; z: number }) =>
|
||||
Math.hypot(c.x - x, c.z - z) <= MUNICAO.raioDeColeta;
|
||||
const recolhidas = p.caixas.filter(perto);
|
||||
for (let i = 0; i < recolhidas.length; i++) {
|
||||
p.jogador.municao += MUNICAO.porCaixa;
|
||||
p.eventos.push({ tipo: "caixa", balas: MUNICAO.porCaixa });
|
||||
}
|
||||
p.caixas = p.caixas.filter((c) => !perto(c));
|
||||
}
|
||||
|
||||
type Cruzamento = { zumbi: Zumbi; t: number };
|
||||
|
||||
/** Zumbis vivos cruzados pelo raio antes do limite, do mais perto para o mais longe. */
|
||||
function cruzamentos(
|
||||
p: Partida,
|
||||
dx: number,
|
||||
dz: number,
|
||||
limite: number,
|
||||
): Cruzamento[] {
|
||||
const lista: Cruzamento[] = [];
|
||||
for (const zumbi of p.zumbis) {
|
||||
if (zumbi.estado === "morrendo") continue;
|
||||
const { x, z } = p.jogador;
|
||||
const t = raioNoCirculo(
|
||||
x,
|
||||
z,
|
||||
dx,
|
||||
dz,
|
||||
zumbi.x,
|
||||
zumbi.z,
|
||||
ZUMBIS[zumbi.tipo].raio,
|
||||
);
|
||||
if (t !== null && t < limite) lista.push({ zumbi, t });
|
||||
}
|
||||
return lista.sort((a, b) => a.t - b.t);
|
||||
}
|
||||
|
||||
/** O primeiro zumbi cuja altura contém a altura do tiro, com essa altura. */
|
||||
function primeiroAtingido(
|
||||
p: Partida,
|
||||
lista: Cruzamento[],
|
||||
): (Cruzamento & { y: number }) | null {
|
||||
const subida = Math.tan(p.jogador.inclinacao);
|
||||
for (const c of lista) {
|
||||
const y = JOGADOR.olhos + subida * c.t;
|
||||
if (y >= 0 && y <= ZUMBIS[c.zumbi.tipo].altura) return { ...c, y };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Distância até a parede (ou o alcance da arma) no rumo atual. */
|
||||
function limiteDoRaio(p: Partida): number {
|
||||
const { x, z, rumo, arma } = p.jogador;
|
||||
return raioAteParede(p.mapa, x, z, rumo, ARMAS[arma].alcance);
|
||||
}
|
||||
|
||||
/** O zumbi que um tiro de raio acertaria agora; `null` com a faca ou sem alvo. */
|
||||
export function alvoNaMira(p: Partida): {
|
||||
zumbi: Zumbi;
|
||||
distancia: number;
|
||||
cabeca: boolean;
|
||||
y: number;
|
||||
} | null {
|
||||
const { rumo, arma } = p.jogador;
|
||||
if (arma === FACA) return null;
|
||||
const lista = cruzamentos(p, Math.sin(rumo), Math.cos(rumo), limiteDoRaio(p));
|
||||
const alvo = primeiroAtingido(p, lista);
|
||||
if (!alvo) return null;
|
||||
const { zumbi, t, y } = alvo;
|
||||
const cabeca = y >= FRACAO_DA_CABECA * ZUMBIS[zumbi.tipo].altura;
|
||||
return { zumbi, distancia: t, cabeca, y };
|
||||
}
|
||||
|
||||
function atirarPorRaio(p: Partida): void {
|
||||
const { x, z, rumo, arma } = p.jogador;
|
||||
const alvo = alvoNaMira(p);
|
||||
p.eventos.push({
|
||||
tipo: "tiro",
|
||||
autor: "jogador",
|
||||
arma,
|
||||
distancia: alvo ? alvo.distancia : limiteDoRaio(p),
|
||||
});
|
||||
if (!alvo) return;
|
||||
const { zumbi, distancia: t, cabeca, y } = alvo;
|
||||
const dx = Math.sin(rumo);
|
||||
const dz = Math.cos(rumo);
|
||||
ferir(
|
||||
p,
|
||||
zumbi,
|
||||
cabeca ? ARMAS[arma].dano * MULTIPLICADOR_DA_CABECA : ARMAS[arma].dano,
|
||||
"jogador",
|
||||
);
|
||||
p.eventos.push({
|
||||
tipo: "acerto",
|
||||
zumbiId: zumbi.id,
|
||||
cabeca,
|
||||
x: x + dx * t,
|
||||
y,
|
||||
z: z + dz * t,
|
||||
});
|
||||
}
|
||||
|
||||
/** Ângulo, de 0 a π, entre o rumo do jogador e a direção até o ponto. */
|
||||
function desvio(p: Partida, zumbi: Zumbi): number {
|
||||
const d =
|
||||
Math.atan2(zumbi.x - p.jogador.x, zumbi.z - p.jogador.z) - p.jogador.rumo;
|
||||
return Math.abs(Math.atan2(Math.sin(d), Math.cos(d)));
|
||||
}
|
||||
|
||||
/** Zumbi vivo mais próximo ao alcance da faca e dentro do arco, com a folga até a borda dele. */
|
||||
function alvoDaFaca(p: Partida): { zumbi: Zumbi; folga: number } | null {
|
||||
let melhor: { zumbi: Zumbi; folga: number } | null = null;
|
||||
for (const zumbi of p.zumbis) {
|
||||
if (zumbi.estado === "morrendo") continue;
|
||||
const folga =
|
||||
Math.hypot(zumbi.x - p.jogador.x, zumbi.z - p.jogador.z) -
|
||||
ZUMBIS[zumbi.tipo].raio;
|
||||
const vale =
|
||||
folga <= ARMAS[FACA].alcance && desvio(p, zumbi) <= ARCO_DA_FACA;
|
||||
if (vale && (melhor === null || folga < melhor.folga))
|
||||
melhor = { zumbi, folga };
|
||||
}
|
||||
return melhor;
|
||||
}
|
||||
|
||||
function golpearComFaca(p: Partida): void {
|
||||
const alvo = alvoDaFaca(p);
|
||||
p.eventos.push({
|
||||
tipo: "tiro",
|
||||
autor: "jogador",
|
||||
arma: FACA,
|
||||
distancia: alvo ? alvo.folga : ARMAS[FACA].alcance,
|
||||
});
|
||||
if (!alvo) return;
|
||||
const { zumbi } = alvo;
|
||||
ferir(p, zumbi, ARMAS[FACA].dano, "jogador");
|
||||
p.eventos.push({
|
||||
tipo: "acerto",
|
||||
zumbiId: zumbi.id,
|
||||
cabeca: false,
|
||||
x: zumbi.x,
|
||||
y: ZUMBIS[zumbi.tipo].altura / 2,
|
||||
z: zumbi.z,
|
||||
});
|
||||
}
|
||||
|
||||
export function atirar(p: Partida): void {
|
||||
const j = p.jogador;
|
||||
const arma = ARMAS[j.arma];
|
||||
if (j.recarga > 0 || (arma.gasta && j.municao === 0)) return;
|
||||
j.recarga = arma.intervalo;
|
||||
if (arma.gasta) j.municao -= 1;
|
||||
if (j.arma === FACA) golpearComFaca(p);
|
||||
else atirarPorRaio(p);
|
||||
}
|
||||
Reference in new issue
Block a user