parent
02f70808b5
commit
bc609b6214
@ -0,0 +1,54 @@
|
|||||||
|
.modal {
|
||||||
|
width: 100vw;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 0.5em 2em;
|
||||||
|
min-height: 4em;
|
||||||
|
width: fit-content;
|
||||||
|
border: 1px solid gray;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0px 1em 1em -1em black;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputs input {
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
border: 1px solid gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-data {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-data h3 {
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-block: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-data ul {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
import { LobbyClient } from "boardgame.io/client";
|
||||||
|
import { LOBBY_SERVER } from "../consts.js";
|
||||||
|
import { Show, createResource, createSignal } from "solid-js";
|
||||||
|
import classes from "./Connect.module.css";
|
||||||
|
import Status from "./Status.tsx";
|
||||||
|
import { DEFAULT_SETUP_DATA, type SetupData } from "../game.ts";
|
||||||
|
|
||||||
|
export type ConnectProps = {
|
||||||
|
gameName: string,
|
||||||
|
matchID: string,
|
||||||
|
setCredentials: (credentials: string) => void,
|
||||||
|
setPlayerID: (playerID: string) => void,
|
||||||
|
setSpectating: (spectating: boolean) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Connect(props: ConnectProps) {
|
||||||
|
const client = new LobbyClient({
|
||||||
|
server: LOBBY_SERVER
|
||||||
|
});
|
||||||
|
const [name, setName] = createSignal<string>("");
|
||||||
|
|
||||||
|
const [match] = createResource(() => client.getMatch(props.gameName, props.matchID));
|
||||||
|
|
||||||
|
return <div class={classes.modal}>
|
||||||
|
<div class={classes.card}>
|
||||||
|
<h2>Join game</h2>
|
||||||
|
|
||||||
|
<div class={classes.inputs}>
|
||||||
|
<input type="text" value={name()} onChange={(event) => setName(event.currentTarget.value)} placeholder="Your name" />
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
client.joinMatch(
|
||||||
|
props.gameName,
|
||||||
|
props.matchID,
|
||||||
|
{
|
||||||
|
playerName: name() || `Anonymous`,
|
||||||
|
}
|
||||||
|
).then((res) => {
|
||||||
|
props.setPlayerID(res.playerID);
|
||||||
|
props.setCredentials(res.playerCredentials);
|
||||||
|
}).catch(console.error);
|
||||||
|
}}
|
||||||
|
disabled={match()?.players.every((player) => !!player.name) ?? false}
|
||||||
|
>
|
||||||
|
Join game
|
||||||
|
</button>
|
||||||
|
<button onClick={() => props.setSpectating(true)}>Spectate</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Status gameName={props.gameName} matchID={props.matchID} />
|
||||||
|
|
||||||
|
<Show when={(match()?.setupData as SetupData)}>
|
||||||
|
{(setupData) => {
|
||||||
|
return (<div class={classes["setup-data"]}>
|
||||||
|
<h3>Game settings:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Size: {setupData().size ?? DEFAULT_SETUP_DATA.size}</li>
|
||||||
|
<li>Initial resources: {setupData().initialResources ?? DEFAULT_SETUP_DATA.initialResources}</li>
|
||||||
|
</ul>
|
||||||
|
</div>);
|
||||||
|
}}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
.game {
|
||||||
|
display: grid;
|
||||||
|
grid-template:
|
||||||
|
"board players" auto
|
||||||
|
"toolbar toolbar" auto
|
||||||
|
/ auto 20ch;
|
||||||
|
margin-left: 1em;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board {
|
||||||
|
grid-area: board;
|
||||||
|
}
|
||||||
|
|
||||||
|
.players {
|
||||||
|
grid-area: players;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
grid-area: toolbar;
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
.matchmaker {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1em;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting > input[type="number"] {
|
||||||
|
appearance: textfield;
|
||||||
|
margin-left: 1em;
|
||||||
|
width: 4em;
|
||||||
|
background-color: transparent;
|
||||||
|
color: white;
|
||||||
|
border: 1px solid gray;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
transition: 0.2s background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting > input[type="number"]:hover {
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
import { LobbyClient } from "boardgame.io/client";
|
||||||
|
import { BASE_URL, DOMAIN, LOBBY_SERVER } from "../consts.ts";
|
||||||
|
import { AcrossTheHex, SetupData } from "../game.ts";
|
||||||
|
import classes from "./MatchMaker.module.css";
|
||||||
|
import { createSignal } from "solid-js";
|
||||||
|
|
||||||
|
|
||||||
|
export default function MatchMaker() {
|
||||||
|
const client = new LobbyClient({
|
||||||
|
server: LOBBY_SERVER
|
||||||
|
});
|
||||||
|
|
||||||
|
const [gridSize, setGridSize] = createSignal(4);
|
||||||
|
const [initialResources, setInitialResources] = createSignal(2);
|
||||||
|
|
||||||
|
return <div class={classes.matchmaker}>
|
||||||
|
<div class={classes.setting}>
|
||||||
|
Grid size:
|
||||||
|
<input type="number" value={gridSize()} min={1} max={20} onChange={(event) => setGridSize(+event.currentTarget.value)} />
|
||||||
|
</div>
|
||||||
|
<div class={classes.setting}>
|
||||||
|
Initial resources:
|
||||||
|
<input type="number" value={initialResources()} min={1} max={100} onChange={(event) => setInitialResources(+event.currentTarget.value)} />
|
||||||
|
</div>
|
||||||
|
<div class={classes.buttons}>
|
||||||
|
<button onClick={() => {
|
||||||
|
const setupData: SetupData = {
|
||||||
|
size: gridSize(),
|
||||||
|
initialResources: initialResources()
|
||||||
|
};
|
||||||
|
client.createMatch(AcrossTheHex.name!, {
|
||||||
|
numPlayers: 2,
|
||||||
|
setupData,
|
||||||
|
}).then((match) => {
|
||||||
|
window.location.href = `${DOMAIN}${BASE_URL}play?match=${match.matchID}`;
|
||||||
|
});
|
||||||
|
}}>Create multiplayer match</button>
|
||||||
|
<button disabled>Play locally</button>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
@ -1,26 +0,0 @@
|
|||||||
---
|
|
||||||
import Game from "./Game.tsx";
|
|
||||||
|
|
||||||
export type Props = {
|
|
||||||
playerID: string | undefined,
|
|
||||||
matchID: string,
|
|
||||||
debug: boolean,
|
|
||||||
};
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
{
|
|
||||||
Astro.props.playerID
|
|
||||||
? <Game
|
|
||||||
playerID={Astro.props.playerID}
|
|
||||||
matchID={Astro.props.matchID}
|
|
||||||
debug={Astro.props.debug}
|
|
||||||
client:only
|
|
||||||
/>
|
|
||||||
: <Game
|
|
||||||
matchID={Astro.props.matchID}
|
|
||||||
debug={Astro.props.debug}
|
|
||||||
client:only
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
<!-- <GameStatus playerID={Astro.props.playerID} matchID={Astro.props.matchID} /> -->
|
|
@ -0,0 +1,48 @@
|
|||||||
|
import { Match, Switch, createSignal } from "solid-js";
|
||||||
|
import Game from "./Game.js";
|
||||||
|
import Connect from "./Connect.tsx";
|
||||||
|
import { AcrossTheHex } from "../game.ts";
|
||||||
|
|
||||||
|
export type MultiplayerProps = {
|
||||||
|
matchID: string,
|
||||||
|
debug: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Multiplayer(props: MultiplayerProps) {
|
||||||
|
const [credentials, setCredentials] = createSignal<string>();
|
||||||
|
const [playerID, setPlayerID] = createSignal<string>();
|
||||||
|
const [spectating, setSpectating] = createSignal(false);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Match when={playerID() && credentials()}>
|
||||||
|
{(_) => {
|
||||||
|
return <Game
|
||||||
|
playerID={playerID()!}
|
||||||
|
credentials={credentials()!}
|
||||||
|
matchID={props.matchID}
|
||||||
|
debug={props.debug}
|
||||||
|
/>;
|
||||||
|
}}
|
||||||
|
</Match>
|
||||||
|
<Match when={!spectating()}>
|
||||||
|
<Connect
|
||||||
|
gameName={AcrossTheHex.name!}
|
||||||
|
matchID={props.matchID}
|
||||||
|
setCredentials={setCredentials}
|
||||||
|
setPlayerID={setPlayerID}
|
||||||
|
setSpectating={setSpectating}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={spectating()}>
|
||||||
|
{(_) => {
|
||||||
|
return <Game
|
||||||
|
matchID={props.matchID}
|
||||||
|
debug={props.debug}
|
||||||
|
/>;
|
||||||
|
}}
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
.users {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
border: 1px solid black;
|
||||||
|
width: 20ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.users > li:not(:first-child) {
|
||||||
|
border-top: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0.75em;
|
||||||
|
height: 0.75em;
|
||||||
|
background: var(--color);
|
||||||
|
margin-inline: 0.5em;
|
||||||
|
line-height: 0.75em;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connected {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logged-out {
|
||||||
|
color: gray;
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
import { LobbyClient } from "boardgame.io/client"
|
||||||
|
import { LOBBY_SERVER, PLAYER_COLORS } from "../consts.ts";
|
||||||
|
import { For, Show, createEffect, createResource, onCleanup } from "solid-js";
|
||||||
|
import classes from "./Status.module.css";
|
||||||
|
|
||||||
|
export type StatusProps = {
|
||||||
|
gameName: string,
|
||||||
|
matchID: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Status(props: StatusProps) {
|
||||||
|
const client = new LobbyClient({
|
||||||
|
server: LOBBY_SERVER
|
||||||
|
});
|
||||||
|
|
||||||
|
const [match, actions] = createResource(() => client.getMatch(props.gameName, props.matchID));
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
actions.refetch();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
actions.refetch();
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
onCleanup(() => clearInterval(interval));
|
||||||
|
});
|
||||||
|
|
||||||
|
return <ul class={classes.users}>
|
||||||
|
<For each={match()?.players} fallback={<li><i>Loading...</i></li>}>
|
||||||
|
{(player, index) => {
|
||||||
|
return <li data-index={index()} class={player.isConnected ? classes.connected : classes["logged-out"]}>
|
||||||
|
<div class={classes["player-icon"]} style={{
|
||||||
|
"--color": PLAYER_COLORS[player.id % PLAYER_COLORS.length]
|
||||||
|
}}></div>
|
||||||
|
<Show when={player.name} fallback={<i>No player</i>}>{player.name}</Show>
|
||||||
|
</li>;
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</ul>;
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
import Document from "../components/Document.astro";
|
import Document from "../components/Document.astro";
|
||||||
import Game from "../components/Game.tsx";
|
import MatchMaker from "../components/MatchMaker.tsx";
|
||||||
|
|
||||||
export const prerender = true;
|
export const prerender = true;
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Document>
|
<Document>
|
||||||
<h1>Across the Hex</h1>
|
<MatchMaker client:only />
|
||||||
</Document>
|
</Document>
|
||||||
|
Loading…
Reference in new issue