Files
games/packages/client/src/components/Table.tsx
2025-08-27 23:04:49 -04:00

129 lines
2.9 KiB
TypeScript

import type { TWsIn, TWsOut } from "@games/server/src/table";
import { fromPromise, Stream } from "kefir";
import {
Accessor,
createContext,
createEffect,
createSignal,
For,
onCleanup,
Show,
} from "solid-js";
import api, { fromWebsocket } from "~/api";
import { createObservable, createObservableWithInit, cx } from "~/fn";
import { me, mePromise } from "~/profile";
import Game from "./Game";
import Player from "./Player";
export const TableContext = createContext<{
view: Accessor<any>;
sendWs: (msg: TWsIn) => void;
wsEvents: Stream<TWsOut, any>;
}>();
export default (props: { tableKey: string }) => {
const wsPromise = new Promise<
ReturnType<ReturnType<typeof api.ws>["subscribe"]>
>((res) => {
const ws = api.ws(props).subscribe();
ws.on("open", () => res(ws));
ws.on("error", () => res(ws));
});
const sendWs = (msg: TWsIn) => wsPromise.then((ws) => ws.send(msg));
const wsEvents = fromPromise(wsPromise).flatMap((ws) =>
fromWebsocket<TWsOut>(ws)
);
onCleanup(() => wsPromise.then((ws) => ws.close()));
const presenceEvents = wsEvents.filter((evt) => evt.players != null);
const gameEvents = wsEvents.filter((evt) => evt.view !== undefined);
const players = createObservableWithInit<string[]>(
presenceEvents.map((evt) => evt.players!),
[]
);
const [ready, setReady] = createSignal(false);
mePromise.then(
(me) =>
me &&
wsEvents
.filter((evt) => evt.playersReady !== undefined)
.map((evt) => evt.playersReady?.[me] ?? false)
.onValue(setReady)
);
createEffect(() => sendWs({ ready: ready() }));
const view = createObservable(gameEvents.map((evt) => evt.view));
return (
<TableContext.Provider
value={{
sendWs,
wsEvents,
view,
}}
>
<div class="flex justify-around p-t-14">
<For each={players().filter((p) => p != me())}>
{(player, i) => {
const verticalOffset = () => {
const N = players().length - 1;
const x = Math.abs((2 * i() + 1) / (N * 2) - 0.5);
const y = Math.sqrt(1 - x * x);
return 1 - y;
};
return (
<Player
playerKey={player}
style={{
transform: `translate(0, ${
verticalOffset() * 150
}vh)`,
}}
/>
);
}}
</For>
</div>
<div
id="table"
class={cx(
"fixed",
"bg-radial",
"from-orange-950",
"to-stone-950",
"border-4",
"border-neutral-950",
"shadow-lg",
"top-40",
"bottom-20",
"left-10",
"right-10"
)}
style={{
"border-radius": "50%",
}}
>
<Show when={view() == null}>
<div class="absolute center">
<button
onClick={() => setReady((prev) => !prev)}
class="button p-1 "
>
{ready() ? "Not Ready" : "Ready"}
</button>
</div>
</Show>
</div>
<Show when={view() != null}>
<Game />
</Show>
</TableContext.Provider>
);
};