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 Game from "../components/Game.tsx";
|
||||
import MatchMaker from "../components/MatchMaker.tsx";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
---
|
||||
|
||||
<Document>
|
||||
<h1>Across the Hex</h1>
|
||||
<MatchMaker client:only />
|
||||
</Document>
|
||||
|
Loading…
Reference in new issue