Files
picobook/src/client/GamePage.tsx
T
2026-06-11 18:39:38 -04:00

197 lines
4.3 KiB
TypeScript

import { Link, useParams, useSearchParams } from "react-router-dom";
import { MutableRefObject, useEffect, useRef, useState } from "react";
import { css } from "@emotion/css";
import { useWebsocket } from "./hooks/useWebsocket";
import { Pico8Player } from "@athingperday/react-pico-player";
type PicoPlayerHandle =
NonNullable<
Parameters<typeof Pico8Player>["0"]["consoleRef"]
> extends MutableRefObject<infer T>
? NonNullable<T>
: never;
type Game = {
carts: Parameters<typeof Pico8Player>["0"]["carts"];
};
const getPatch = (before: number[], after: number[]) => {
const diff: Record<number, number> = {};
for (const i in after) {
if (after[i] !== before[i]) {
diff[i] = after[i];
}
}
return diff;
};
const applyPatch = (before: number[], patch: Record<number, number>) => {
for (const i in patch) {
before[i] = patch[i];
}
};
export const GamePage = () => {
const { author, slug } = useParams();
// const [text, setText] = useState("");
const [prevGpio, setPrevGpio] = useState<number[] | null>(null);
const [searchParams, setSearchParams] = useSearchParams();
const room = searchParams.get("room");
const picoRef = useRef<PicoPlayerHandle>(null);
const socket = useWebsocket({
url: `/api/ws/room?room=${room}`,
onMessage({ message }) {
// console.log("message", message);
const msg = message as any;
if (msg.getGpio) {
if (picoRef.current) {
const handle = picoRef.current;
if (handle) {
socket.sendMessage({ gpio: handle.gpio });
}
}
}
if (msg.gpio) {
if (picoRef.current) {
const handle = picoRef.current;
if (handle) {
// console.log("updating pico gpio");
handle.gpio.length = 0;
handle.gpio.push(...msg.gpio);
setPrevGpio([...handle.gpio]);
}
}
}
if (msg.gpioPatch) {
if (picoRef.current) {
const handle = picoRef.current;
if (handle) {
// console.log("updating pico gpio");
applyPatch(handle.gpio, msg.gpioPatch);
setPrevGpio([...handle.gpio]);
}
}
}
},
});
const [game, setGame] = useState<Game | null>(null);
useEffect(() => {
const fetchInfo = async () => {
let url = `/api/game?author=${author}&name=${slug ?? ""}`;
const information = await fetch(url);
const json = await information.json();
console.log(json);
setGame(json);
};
fetchInfo();
}, [setGame, slug]);
useEffect(() => {
const interval = setInterval(() => {
const handle = picoRef.current;
if (!handle) {
return;
}
if (JSON.stringify(handle.gpio) !== JSON.stringify(prevGpio)) {
if (prevGpio) {
setPrevGpio([...handle.gpio]);
socket.sendMessage({
gpioPatch: getPatch(prevGpio, handle.gpio),
});
} else {
socket.sendMessage({ getGpio: true });
setPrevGpio([...handle.gpio]);
}
}
}, 1000 / 60);
return () => {
clearInterval(interval);
};
});
if (!game) {
return <div>LOADING...</div>;
}
if (!game.carts) {
return <div>NOT FOUND</div>;
}
return (
<div
className={css`
margin: auto;
width: max-content;
max-inline-size: 66ch;
padding: 1.5em;
display: flex;
flex-direction: column;
gap: 1em;
`}
>
<div>
<h1>{slug}</h1>
<h2>
by <Link to={`/u/${author}`}>{author}</Link>
</h2>
</div>
<div
className={css`
width: 512px;
max-width: 100%;
margin: auto;
`}
>
<div
className={css`
border: 2px solid transparent;
&:focus-within {
border: 2px solid limegreen;
}
`}
>
<Pico8Player consoleRef={picoRef} carts={game.carts} />
</div>
</div>
<div
className={css`
display: flex;
flex-wrap: wrap;
`}
>
{game.carts.map((cart) =>
"src" in cart ? (
<div
key={cart.name}
className={css`
display: flex;
flex-direction: column;
`}
>
<img src={cart.src} />
<a href={cart.src} download={`${cart.name}.p8.png`}>
Download
</a>
</div>
) : null,
)}
</div>
{/* <div>
<input onChange={(x) => setText(x.target.value)} />
<button
onClick={() => {
socket.sendMessage({ text });
}}
>
Send
</button>
</div> */}
{/* <div>
<p>This is a paragraph about this game. It is a cool game. And a cool website to play it on. It automagically connects from GitHub.</p>
</div> */}
</div>
);
};