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.
207 lines
5.7 KiB
TypeScript
207 lines
5.7 KiB
TypeScript
/** 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);
|
|
}
|