Files
portifolio-evandrodevbr/app/jogo/Toque.tsx
T
evandro 67b32fcab3
CI / audit (push) Successful in 10m48s
CI / verify (push) Successful in 19m2s
CI / deploy (push) Successful in 2m14s
CI / e2e (push) Failing after 36m15s
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.
2026-11-10 19:37:10 -03:00

170 lines
5.3 KiB
TypeScript

"use client";
import { type PointerEvent, useEffect, useRef, useState } from "react";
import { alavanca } from "@/lib/jogo/alavanca";
import type { ArmaId } from "@/lib/jogo/tipos";
import type { Controle } from "./contrato";
import {
TOQUE_ATIRAR,
TOQUE_BOTAO,
TOQUE_DIREITA,
TOQUE_EMBAIXO,
TOQUE_ESPACO,
} from "./medidas";
const RAIO = 56;
const MIRA = 0.005;
const FACA: ArmaId = 2;
// right/bottom e tamanhos dos botões vêm de ./medidas, que o Hud também lê.
/** "tiro" é o dedo no botão Atirar: segura o tiro e também mira. */
type Dedo = { papel: "mover" | "mirar" | "tiro"; x: number; y: number };
type Direcional = { x: number; y: number; kx: number; ky: number };
const BOTAO =
"pointer-events-auto flex min-w-14 flex-col items-center justify-center border bg-[color:var(--ink)]/30 px-3 font-[family-name:var(--font-mono)] text-[13px] leading-tight tracking-[0.02em] text-[color:var(--paper)] select-none";
export function Toque({
controle,
arma,
municao,
mirando,
}: {
controle: Controle;
arma: ArmaId;
municao: number;
mirando: boolean;
}) {
const dedos = useRef(new Map<number, Dedo>());
const apertado = useRef(false);
const automatico = useRef(false);
const [direcional, setDirecional] = useState<Direcional | null>(null);
useEffect(() => {
automatico.current = mirando && arma !== FACA;
controle.definirAtirar(apertado.current || automatico.current);
}, [controle, mirando, arma]);
function aoApertar(e: PointerEvent<HTMLDivElement>) {
const papel = (e.target as HTMLElement).closest<HTMLElement>("[data-papel]")
?.dataset.papel;
if (papel === "arma") return;
const { clientX: x, clientY: y, pointerId } = e;
if (papel === "atirar") {
apertado.current = true;
controle.definirAtirar(true);
dedos.current.set(pointerId, { papel: "tiro", x, y });
return;
}
const lado = x < window.innerWidth / 2 ? "mover" : "mirar";
const ocupado = [...dedos.current.values()].some((d) => d.papel === lado);
if (ocupado) return;
dedos.current.set(pointerId, { papel: lado, x, y });
if (lado === "mover") setDirecional({ x, y, kx: 0, ky: 0 });
}
function aoMover(e: PointerEvent<HTMLDivElement>) {
const dedo = dedos.current.get(e.pointerId);
if (!dedo) return;
const dx = e.clientX - dedo.x;
const dy = e.clientY - dedo.y;
if (dedo.papel !== "mover") {
controle.somarOlhar(-dx * MIRA, -dy * MIRA);
dedo.x = e.clientX;
dedo.y = e.clientY;
return;
}
const [direita, frente] = alavanca(dx, dy, RAIO);
controle.definirMover(direita, frente);
setDirecional({
x: dedo.x,
y: dedo.y,
kx: direita * RAIO,
ky: -frente * RAIO,
});
}
function aoSoltar(e: PointerEvent<HTMLDivElement>) {
const dedo = dedos.current.get(e.pointerId);
if (!dedo) return;
dedos.current.delete(e.pointerId);
if (dedo.papel === "tiro") {
apertado.current = false;
controle.definirAtirar(automatico.current);
}
if (dedo.papel === "mover") {
controle.definirMover(0, 0);
setDirecional(null);
}
}
const armas: { id: ArmaId; nome: string }[] = [
{ id: 0, nome: "Pistola" },
{ id: 1, nome: "Automática" },
{ id: 2, nome: "Faca" },
];
return (
<div
role="application"
aria-label="Controles de toque"
className="absolute inset-0 select-none"
style={{ touchAction: "none" }}
onPointerDown={aoApertar}
onPointerMove={aoMover}
onPointerUp={aoSoltar}
onPointerCancel={aoSoltar}
onContextMenu={(e) => e.preventDefault()}
>
{direcional && (
<div
data-testid="direcional"
className="pointer-events-none absolute size-28 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-[color:var(--slash)]/50"
style={{ left: direcional.x, top: direcional.y }}
>
<div
className="absolute top-1/2 left-1/2 size-12 rounded-full bg-[color:var(--slash)]/40"
style={{
transform: `translate(calc(-50% + ${direcional.kx}px), calc(-50% + ${direcional.ky}px))`,
}}
/>
</div>
)}
<div
className="pointer-events-none absolute flex flex-col items-end"
style={{
right: TOQUE_DIREITA,
bottom: TOQUE_EMBAIXO,
gap: TOQUE_ESPACO,
}}
>
<div className="flex" style={{ gap: TOQUE_ESPACO }}>
{armas.map(({ id, nome }) => (
<button
key={id}
type="button"
data-papel="arma"
aria-pressed={arma === id}
onClick={() => controle.definirArma(id)}
style={{ minHeight: TOQUE_BOTAO }}
className={`${BOTAO} ${arma === id ? "border-[color:var(--slash)] bg-[color:var(--slash)]/25" : "border-[color:var(--paper)]/40"}`}
>
{nome}
{id === 1 && <span>{municao}</span>}
</button>
))}
</div>
<button
type="button"
data-papel="atirar"
style={{ width: TOQUE_ATIRAR, height: TOQUE_ATIRAR }}
className={`${BOTAO} border-[color:var(--slash)]/70 active:bg-[color:var(--slash)]/30`}
>
Atirar
</button>
</div>
</div>
);
}