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.
170 lines
5.3 KiB
TypeScript
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>
|
|
);
|
|
}
|