Files
vpinball/components/GameInfoPanel.tsx
T
valknarandClaude Sonnet 5 35fb68c916 Add ROM upload, local table library, engine bump, and UX/responsiveness fixes
- Bump @valknar/vpinball-wasm to 0.3.2 (PinMAME ROM support, MsgBox fix)
- Add optional PinMAME ROM .zip upload alongside .vpx tables
- Persist uploaded tables/ROMs in IndexedDB so users don't have to
  re-upload on later visits, with a library list on the landing page
- Fix Insert Coin key mismatch with the engine's real coin-door gating
  (now bound to 4, matching the shared table scripts) and surface it
  in the controls legend and "Tap to Start" hint
- Make the landing page, staging panel, and in-game HUD responsive for
  portrait phone widths; drop the landscape hint since portrait plays fine
- Replace the separate STATS HUD toggle with an FPS row in the info panel

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9JmzRBZenapVkFWjw9jmG
2026-08-23 20:15:41 +02:00

75 lines
2.5 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { formatBytes, formatDuration } from "@/lib/format";
import { KEY_BINDINGS } from "@/lib/pinball/keyBindings";
import type { TableInfo } from "@/lib/pinball/types";
export interface GameInfoPanelProps {
tableInfo: TableInfo;
loadTimeMs: number | null;
}
/** Self-contained rAF-based FPS counter — the engine exposes no perf API of its own. */
function useFps(): number | null {
const [fps, setFps] = useState<number | null>(null);
useEffect(() => {
let frameCount = 0;
let windowStart = performance.now();
let raf = 0;
const tick = (now: number) => {
frameCount += 1;
const elapsed = now - windowStart;
if (elapsed >= 500) {
setFps(Math.round((frameCount * 1000) / elapsed));
frameCount = 0;
windowStart = now;
}
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, []);
return fps;
}
export default function GameInfoPanel({ tableInfo, loadTimeMs }: GameInfoPanelProps) {
const fps = useFps();
return (
<div className="w-full space-y-4 rounded border border-chrome/30 bg-ash/95 p-4 font-mono text-sm text-paper shadow-xl sm:w-72">
<div>
<div className="text-xs tracking-widest text-chrome">TABLE</div>
<div className="truncate text-arc">{tableInfo.name}</div>
</div>
<dl className="grid grid-cols-2 gap-x-2 gap-y-1 text-xs">
<dt className="text-chrome">FPS</dt>
<dd className="tabular-nums text-arc">{fps ?? "—"}</dd>
<dt className="text-chrome">Source</dt>
<dd>{tableInfo.isDemo ? "Bundled demo" : "Your upload"}</dd>
<dt className="text-chrome">Size</dt>
<dd>{tableInfo.isDemo ? "—" : formatBytes(tableInfo.sizeBytes)}</dd>
<dt className="text-chrome">Load time</dt>
<dd>{loadTimeMs !== null ? formatDuration(loadTimeMs) : "—"}</dd>
<dt className="text-chrome">ROM</dt>
<dd className="truncate">{tableInfo.romFileName ?? "—"}</dd>
</dl>
<div>
<div className="mb-1 text-xs tracking-widest text-chrome">CONTROLS</div>
<ul className="space-y-1 text-xs">
{KEY_BINDINGS.map((binding) => (
<li key={binding.action} className="flex justify-between gap-2">
<span className="text-chrome">{binding.action}</span>
<span className="text-paper">{binding.keys}</span>
</li>
))}
</ul>
</div>
</div>
);
}