197 lines
4.3 KiB
TypeScript
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>
|
|
);
|
|
};
|