- 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
75 lines
2.5 KiB
TypeScript
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>
|
|
);
|
|
}
|