diff --git a/client/public/assets/dice-six-faces-five.svg b/client/public/assets/dice-six-faces-five.svg
new file mode 100755
index 0000000..df55339
--- /dev/null
+++ b/client/public/assets/dice-six-faces-five.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/public/assets/dice-six-faces-four.svg b/client/public/assets/dice-six-faces-four.svg
new file mode 100755
index 0000000..dc22715
--- /dev/null
+++ b/client/public/assets/dice-six-faces-four.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/public/assets/dice-six-faces-one.svg b/client/public/assets/dice-six-faces-one.svg
new file mode 100755
index 0000000..7fb7058
--- /dev/null
+++ b/client/public/assets/dice-six-faces-one.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/public/assets/dice-six-faces-six.svg b/client/public/assets/dice-six-faces-six.svg
new file mode 100755
index 0000000..cf17eae
--- /dev/null
+++ b/client/public/assets/dice-six-faces-six.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/public/assets/dice-six-faces-three.svg b/client/public/assets/dice-six-faces-three.svg
new file mode 100755
index 0000000..7898a78
--- /dev/null
+++ b/client/public/assets/dice-six-faces-three.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/public/assets/dice-six-faces-two.svg b/client/public/assets/dice-six-faces-two.svg
new file mode 100755
index 0000000..a2e6eec
--- /dev/null
+++ b/client/public/assets/dice-six-faces-two.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/public/assets/gfx/tabletop.png b/client/public/assets/gfx/tabletop.png
new file mode 100644
index 0000000..bdf89eb
Binary files /dev/null and b/client/public/assets/gfx/tabletop.png differ
diff --git a/client/public/assets/original/tabletop.png b/client/public/assets/original/tabletop.png
new file mode 100644
index 0000000..ab168d4
Binary files /dev/null and b/client/public/assets/original/tabletop.png differ
diff --git a/client/src/App.js b/client/src/App.js
index 18e5615..1cd5293 100755
--- a/client/src/App.js
+++ b/client/src/App.js
@@ -15,7 +15,7 @@ import {
//import 'typeface-roboto';
//import history from "./history.js";
-import Board from "./Board.js";
+import Table from "./Table.js";
import "./App.css";
let base = process.env.PUBLIC_URL;
@@ -25,8 +25,8 @@ function App() {
return (
- } path={`${base}/games/:id`}/>
- } path={`${base}`}/>
+ } path={`${base}/games/:id`}/>
+ } path={`${base}`}/>
);
diff --git a/client/src/Board.css b/client/src/Board.css
old mode 100755
new mode 100644
index ce9b3a6..596b5f9
--- a/client/src/Board.css
+++ b/client/src/Board.css
@@ -7,333 +7,3 @@
justify-content: right;
}
-.Display {
- display: inline-block;
- position: absolute;
-}
-
-.PlayerColor {
- display: inline-flex;
- justify-content: center;
- align-items: center;
- width: 1em;
- height: 1em;
- padding: 0.125em;
- margin: 0 0.25em;
- border-radius: 0.625em;
- border-width: 1px;
- border-style: solid;
- text-align: center;
-}
-
-.PlayerColor > div {
- font-weight: bold;
- overflow: hidden;
- font-size: 0.75rem;
-}
-
-.Cards {
- display: inline-block;
- position: absolute;
- text-align: right;
- vertical-align: bottom;
- padding: 0.5em;
- box-sizing: border-box;
- max-height: 100%;
- max-width: 100%;
- opacity: 0.7;
-}
-
-.Stack {
- position: relative;
- display: inline-block;
-}
-
-.Stack:not(:first-child) {
- margin-left: -3em;
- transition: margin-left 1s ease-in-out 0.25s;
-}
-
-.Stack > * {
- transition: margin-left 1s ease-in-out 0.25s, margin-right 1s ease-in-out 0.25s;
-}
-
-.Development:hover,
-.Placard:hover,
-.Resource:hover {
- filter: brightness(150%);
-}
-
-.Dice {
- width: 1rem;
- height: 1rem;
- background-color: black;
-}
-
-.Game {
- display: inline-flex;
- flex-direction: column;
- box-sizing: border-box;
- width: 40vw;
- max-height: 100vh;
- overflow: hidden;
- max-width: 40vw;
- z-index: 100;
- padding: 0.5em;
- opacity: 0.7;
-}
-
-.Game > * {
- /* for Firefox */
- min-height: 0;
-}
-
-.Game > *:not(:last-child) {
- margin-bottom: 0.5em;
-}
-
-.Game .lobby {
- width: 100vw;
-}
-
-.Chat {
- display: flex;
- flex-direction: column;
- flex: 1;
- padding: 0.5em;
-}
-
-.Chat > * {
- /* for Firefox */
- min-height: 0;
-}
-
-#ChatList {
- flex: 1;
- overflow: auto;
- scroll-behavior: smooth;
- align-items: flex-start;
-}
-
-#ChatList .MuiListItem-gutters {
- padding: 2px 0 2px 0;
-}
-
-#ChatList .MuiTypography-body1 {
- font-size: 0.8rem;
-}
-
-#ChatList .MuiTypography-body2 {
- font-size: 0.7rem;
-}
-
-#ChatList .MuiListItemText-multiline {
- margin-top: 0;
- margin-bottom: 0;
- padding: 4px 0px 4px 4px;
-}
-
-#ChatList .PlayerColor {
- width: 1em;
- height: 1em;
- padding: 0;
- margin-top: 4px;
-}
-
-.Players {
- padding: 0.5em;
- user-select: none;
-}
-
-.PlayerSelector .PlayerColor {
- width: 1em;
- height: 1em;
-}
-
-.PlayerSelector {
- display: inline-flex;
- flex-wrap: wrap;
- flex-direction: row;
- justify-content: space-between;
-}
-
-.PlayerSelector.MuiList-padding {
- padding: 0;
-}
-
-.PlayerSelector .PlayerEntry {
- flex: 1 1 0px;
- align-items: center;
- display: inline-flex;
- flex-direction: row;
- min-width: 10em;
-}
-
-.PlayerSelector .MuiTypography-body1 {
- font-size: 0.8rem;
- white-space: nowrap;
-}
-
-.PlayerSelector .MuiTypography-body2 {
- font-size: 0.7rem;
- white-space: nowrap;
-}
-
-.Players .PlayerEntry {
- border: 1px solid rgba(0,0,0,0);
- border-radius: 0.5em;
- min-width: 10em;
-}
-
-.Players .PlayerEntry[data-selectable=true]:hover {
- border-color: rgba(0,0,0,0.5);
- cursor: pointer;
-}
-
-.Players .PlayerEntry[data-selected=true] {
- background-color: rgba(255, 255, 0, 0.5);
-}
-
-.Players .PlayerToggle {
- min-width: 5em;
- display: inline-flex;
- align-items: end;
- flex-direction: column;
-}
-
-.PlayerName {
- padding: 0.5em;
-}
-
-.Players > * {
- width: 100%;
-}
-
-.Players .nameInput {
- flex-grow: 1;
-}
-
-.Stack > *:not(:first-child) {
- margin-left: -4.5em;
-}
-
-.Hand {
- min-height: calc(7.2em + 0.5em);
-}
-
-.Hand:hover .Stack:hover > *:not(:first-child) {
- margin-left: -2em;
-}
-
-.Hand:hover .Stack:hover:not(:last-child) > *:last-child {
- margin-right: 3em;
-}
-
-.Placard {
- position: relative;
- width: 9.4em;
- height: 11.44em;
- background-position: center;
- background-repeat: no-repeat;
- background-size: cover;
- margin: 0.25em;
- display: inline-block;
-}
-
-.Development {
- position: relative;
- display: inline-block;
- width: 4.9em;
- height: 7.2em;
- background-position: center;
- background-repeat: no-repeat;
- background-size: cover;
- margin: 0.25em;
-}
-
-.Resource {
- position: relative;
- width: 4.9em;
- height: 7.2em;
- display: inline-block;
- background-position: center;
- background-repeat: no-repeat;
- background-size: cover;
- margin: 0.25em;
-}
-
-.Action {
- display: flex;
- align-items: center;
- justify-content: space-evenly;
- background-color: rgba(16, 16, 16, 0.25);
- padding: 0.25em;
-}
-
-button {
- margin: 0.25em;
- background-color: white;
- border: 1px solid black !important;
-}
-
-.Error {
- display: flex;
- position: absolute;
- top: calc(50vh - 1.5em);
- left: 0px;
- right: 0px;
- align-items: center;
- justify-content: center;
- background-color: yellow;
- text-align: left;
- font-size: 12pt;
- padding: 1em;
- margin: 1em;
- z-index: 10000;
-}
-
-.Message {
- display: inline;
- justify-content: left;
- background-color: rgba(224, 224, 224);
- text-align: left;
- font-size: 12pt;
- padding: 0.5em;
- user-select: none;
-}
-
-.Message .PlayerColor {
- width: 1em;
- height: 1em;
-}
-
-.Message div {
- display: inline-flex;
-}
-
-.PlayerName {
- display: flex;
- align-items: center;
- justify-content: center;
- flex-direction: row;
-}
-
-.PlayerName > .nameInput {
- margin-right: 1em;
- flex: 1;
- max-width: 30em;
-}
-
-.PlayerName > Button {
- background: lightblue;
-}
-
-.Statistics > div:nth-child(2) {
- display: flex;
- flex-direction: row;
- border: 1px solid black;
-}
-
-.Statistics div:nth-child(2) div {
- padding: 0.25em 0.5em;
-}
diff --git a/client/src/Board.js b/client/src/Board.js
old mode 100755
new mode 100644
index 4e4b11e..d61bbe4
--- a/client/src/Board.js
+++ b/client/src/Board.js
@@ -1,97 +1,10 @@
-import React, { useState, useEffect } from "react";
+import React, { useCallback, useMemo, useRef, useState, useEffect } from "react";
import "./Board.css";
-import history from "./history.js";
-import Paper from '@material-ui/core/Paper';
-import Button from '@material-ui/core/Button';
-import TextField from '@material-ui/core/TextField';
-import List from '@material-ui/core/List';
-import ListItem from '@material-ui/core/ListItem';
-import ListItemAvatar from '@material-ui/core/ListItemAvatar';
-import ListItemText from '@material-ui/core/ListItemText';
-import { makeStyles } from '@material-ui/core/styles';
-import { orange, deepOrange, lightBlue, red, grey } from '@material-ui/core/colors';
-import Avatar from '@material-ui/core/Avatar';
-import Moment from 'react-moment';
-//import moment from 'moment';
-/* Start of withRouter polyfill */
-// https://reactrouter.com/docs/en/v6/faq#what-happened-to-withrouter-i-need-it
-import {
- useLocation,
- useNavigate,
- useParams
-} from "react-router-dom";
-
-function withRouter(Component) {
- function ComponentWithRouterProp(props) {
- let location = useLocation();
- let navigate = useNavigate();
- let params = useParams();
- return (
-
- );
- }
-
- return ComponentWithRouterProp;
-}
-/* end of withRouter polyfill */
-
-const base = process.env.PUBLIC_URL;
-
+const base = process.env.PUBLIC_URL;
const assetsPath = `${base}/assets`;
-const gamesPath = `${base}/games`;
const images = {};
-const useStyles = makeStyles((theme) => ({
- root: {
- display: 'flex',
- '& > *': {
- margin: theme.spacing(1),
- },
- },
- R: {
- color: theme.palette.getContrastText(red[500]),
- backgroundColor: red[500],
- },
- O: {
- color: theme.palette.getContrastText(orange[500]),
- backgroundColor: orange[500],
- },
- W: {
- color: theme.palette.getContrastText(grey[500]),
- backgroundColor: grey[500],
- },
- B: {
- color: theme.palette.getContrastText(lightBlue[500]),
- backgroundColor: lightBlue[500],
- },
-}));
-
-const Dice = ({ pips }) => {
- let name;
- switch (pips) {
- case 1: name = 'one'; break;
- case 2: name = 'two'; break;
- case 3: name = 'three'; break;
- case 4: name = 'four'; break;
- case 5: name = 'five'; break;
- case 6: name = 'six'; break;
- }
- return (
-
- );
-}
-
-const PlayerColor = ({ color }) => {
- const classes = useStyles();
- return (
-
- );
-};
-
const getPlayerColors = (color) => {
switch (color) {
case "O":
@@ -122,8 +35,6 @@ const getPlayerColors = (color) => {
};
};
-
-
const hexagonRatio = 1.1547005,
tileHeight = 0.16,
tileWidth = tileHeight * hexagonRatio,
@@ -141,28 +52,55 @@ const hexagonRatio = 1.1547005,
angle: 0
} ];
+const imageLoaded = (event) => {
+ const image = event.target;
+ console.log(`Done loading ${image.src}`);
+ image.removeEventListener("load", imageLoaded);
+ image.removeEventListener("error", imageLoadError);
-const loadImage = (board, file) => {
+ try {
+ if (image.drawFrame) {
+ window.requestAnimationFrame(image.drawFrame);
+ }
+ } catch (error) {
+ image.board = null;
+ }
+};
+
+const imageLoadError = (event) => {
+ const image = event.target;
+ console.log(`Error loading ${image.src}`);
+ image.removeEventListener("load", imageLoaded);
+ image.removeEventListener("error", imageLoadError);
+ console.log(`Error loading ${image.src}`);
+}
+
+const loadImage = (file, drawFrame) => {
if (file in images) {
+ images[file].drawFrame = drawFrame;
return images[file];
}
const image = new Image();
+ image.drawFrame = drawFrame;
images[file] = image;
- image.addEventListener("load", board.imageLoaded);
- image.addEventListener("error", board.imageLoadError);
+ image.addEventListener("load", imageLoaded);
+ image.addEventListener("error", imageLoadError);
image.src = `${assetsPath}/gfx/${file}`;
return image;
}
-const Tiles = (board) => {
- const tiles = board.game.tiles;
+const Tiles = (game, drawFrame) => {
+ if (!game) {
+ return;
+ }
+ const tiles = game.tiles;
[ "robber", "brick", "wood", "sheep", "stone", "wheat" ].forEach((type) => {
const file = "tiles-" + type + ".png",
- image = loadImage(board, file);
+ image = loadImage(file, drawFrame);
tiles.forEach((tile) => {
if (tile.type === type) {
@@ -177,812 +115,43 @@ const Tiles = (board) => {
return tiles;
};
-const Pips = (board) => {
- const file = 'pip-numbers.png',
- image = loadImage(board, file);
+const Board = ({ game }) => {
+ const [mouse] = useState({x: 0, y: 0});
+ const [pips, setPips] = useState([]);
+ const [borders, setBorders] = useState([]);
+ const [tabletop, setTabletop] = useState(null);
- return {
- image: image,
- pips: board.game.pips
- };
-};
+ const radius = 0.317;
+ const canvasRef = useRef(null);
-const Border = (board, border) => {
- const file = border.file,
- image = loadImage(board, file);
+ let minSize;
- border.image = image;
- return border;
-};
-
-const Tabletop = (board) => {
- const file = "tabletop.png",
- image = loadImage(board, file);
-
- return image;
-};
-
-class Placard extends React.Component {
- render() {
- return (
-
-
- );
- }
-};
-
-class Development extends React.Component {
- render() {
- const array = [];
- for (let i = 0; i < this.props.count; i++) {
- if (this.props.type.match(/-$/)) {
- array.push(i + 1);//Math.ceil(Math.random() * this.props.max));
- } else {
- array.push("");
- }
- }
- return (
-
- { React.Children.map(array, i => (
-
-
- )) }
-
- );
- }
-};
-
-class Resource extends React.Component {
- render() {
- const array = new Array(Number(this.props.count ? this.props.count : 0));
- return (
- <>
- { array.length > 0 &&
-
- { React.Children.map(array, i => (
-
-
- )) }
-
- }
- >
- );
- }
-};
-
-const Chat = ({ board, promoteGameState }) => {
- const [lastTop, setLastTop] = useState(0),
- [autoScroll, setAutoscroll] = useState(true),
- [scrollTime, setScrollTime] = useState(0);
-
- const chatInput = (event) => {
- };
-
- const chatKeyPress = (event) => {
- if (event.key === "Enter") {
- if (!autoScroll) {
- setAutoscroll(true);
- }
-
- promoteGameState({
- chat: {
- player: board.game.color ? board.game.color : undefined,
- message: event.target.value
- }
- });
- event.target.value = "";
- }
- };
-
- const chatScroll = (event) => {
- const chatList = event.target,
- fromBottom = Math.round(Math.abs((chatList.scrollHeight - chatList.offsetHeight) - chatList.scrollTop));
-
- /* If scroll is within 20 pixels of the bottom, turn on auto-scroll */
- const shouldAutoscroll = (fromBottom < 20);
-
- if (shouldAutoscroll !== autoScroll) {
- setAutoscroll(shouldAutoscroll);
- }
-
- /* If the list should not auto scroll, then cache the current
- * top of the list and record when we did this so we honor
- * the auto-scroll for at least 500ms */
- if (!shouldAutoscroll) {
- const target = Math.round(chatList.scrollTop);
- if (target !== lastTop) {
- setLastTop(target);
- setScrollTime(Date.now());
- }
- }
- };
-
- useEffect(() => {
- const chatList = document.getElementById("ChatList"),
- currentTop = Math.round(chatList.scrollTop);
-
- if (autoScroll) {
- /* Auto-scroll to the bottom of the chat window */
- const target = Math.round(chatList.scrollHeight - chatList.offsetHeight);
- if (currentTop !== target) {
- chatList.scrollTop = target;
- }
+ const drawFrame = useCallback(() => {
+ if (!canvasRef || !tabletop) {
+ console.log("TODO: Put this in the correct location instead of drawFrame");
+ updateGame(game);
return;
}
- /* Maintain current position in scrolled view if the user hasn't
- * been scrolling in the past 0.5s */
- if ((Date.now() - scrollTime) > 500 && currentTop !== lastTop) {
- chatList.scrollTop = lastTop;
- }
- });
-
- //const timeDelta = game.timestamp - Date.now();
- const messages = board.game.chat.map((item, index) => {
- /* If the date is in the future, set it to now */
- const name = getPlayerName(board.game.sessions, item.from),
- from = name ? `${name}, ` : '';
- return (
-
-
- {from}
- Date.now() ?
- Date.now() : item.date} interval={1000}/>>)} />
-
- );
- });
-
- return (
-
-
- { messages }
-
- )} variant="outlined"/>
-
- );
-}
-
-const StartButton = ({ board }) => {
- const startClick = (event) => {
- board.setGameState("game-order").then((state) => {
- board.game.state = state;
- });
- };
-
- return (
-
- );
-};
-
-const Action = ({ board }) => {
- const newBoardClick = (event) => {
- return board.shuffleBoard();
- };
-
- const rollClick = (event) => {
- board.throwDice();
- }
-
- const passClick = (event) => {
- }
-
- return (
-
- { board.game.state === 'lobby' && <>
-
-
- > }
- { board.game.state === 'game-order' &&
- }
- { board.game.state === 'active' && <>
-
-
- > }
-
- );
-}
-
-const PlayerName = ({board}) => {
- const [name, setName] = useState((board && board.game && board.game.name) ? board.game.name : "");
-
- const nameChange = (event) => {
- setName(event.target.value);
- }
-
- const sendName = () => {
- console.log(`Send: ${name}`);
- if (name !== board.game.name) {
- board.setPlayerName(name);
- } else {
- board.setState({ pickName: false, error: "" });
- }
- }
-
- const nameKeyPress = (event) => {
- if (event.key === "Enter") {
- sendName();
- }
- }
-
- return (
-
-
-
-
- );
-};
-
-const getPlayerName = (sessions, color) => {
- for (let i = 0; i < sessions.length; i++) {
- const session = sessions[i];
- if (session.color === color) {
- return session.name;
- }
- }
- return null;
-}
-
-/* This needs to take in a mechanism to declare the
- * player's active item in the game */
-const Players = ({ board }) => {
- const toggleSelected = (key) => {
- console.log('toggle');
- board.setSelected(board.game.color === key ? "" : key);
- }
-
- const players = [];
- for (let color in board.game.players) {
- const item = board.game.players[color], inLobby = board.game.state === 'lobby';
- if (!inLobby && item.status === 'Not active') {
- continue;
- }
- const name = getPlayerName(board.game.sessions, color),
- selectable = board.game.state === 'lobby' && (item.status === 'Not active' || board.game.color === color);
- let toggleText = name ? name : "Available";
- players.push((
- { inLobby && selectable && toggleSelected(color) }}
- key={`player-${color}`}>
-
-
- { item.status + ' ' }
- { item.status !== 'Not active' && Date.now() ? Date.now() : item.lastActive}/>}
- >)} />
- { !inLobby && board.game.color === color &&
-
- }
-
- ));
- }
-
- return (
-
-
- { players }
-
-
- );
-}
-
-class Board extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- total: 0,
- wood: 0,
- sheep: 0,
- brick: 0,
- stone: 0,
- wheat: 0,
- game: null,
- message: "",
- error: ""
- };
- this.componentDidMount = this.componentDidMount.bind(this);
- this.updateDimensions = this.updateDimensions.bind(this);
- this.drawFrame = this.drawFrame.bind(this);
- this.drawBorders = this.drawBorders.bind(this);
- this.drawPips = this.drawPips.bind(this);
- this.drawDie = this.drawDie.bind(this);
- this.mouseMove = this.mouseMove.bind(this);
- this.throwDice = this.throwDice.bind(this);
- this.promoteGameState = this.promoteGameState.bind(this);
- this.resetGameLoad = this.resetGameLoad.bind(this);
- this.loadGame = this.loadGame.bind(this);
- this.rollDice = this.rollDice.bind(this);
- this.setGameState = this.setGameState.bind(this);
- this.shuffleBoard = this.shuffleBoard.bind(this);
- this.updateGame = this.updateGame.bind(this);
- this.imageLoadError = this.imageLoadError.bind(this);
- this.imageLoaded = this.imageLoaded.bind(this);
- this.setPlayerName = this.setPlayerName.bind(this);
- this.setSelected = this.setSelected.bind(this);
- this.updateMessage = this.updateMessage.bind(this);
-
- this.mouse = { x: 0, y: 0 };
- this.radius = 0.317;
-
- this.loadTimer = null;
-
- this.game = null;
- this.pips = [];
- this.tiles = [];
- this.borders = [];
- this.tabletop = null;
- this.closest = {
- info: {},
- tile: null,
- road: null,
- tradeToken: null,
- settlement: null
- };
-
- this.id = (props.router && props.router.params.id) ? props.router.params.id : 0;
- }
-
- imageLoaded(event) {
- const image = event.target;
- console.log(`Done loading ${image.src}`);
- image.removeEventListener("load", this.imageLoaded);
- image.removeEventListener("error", this.imageLoadError);
- window.requestAnimationFrame(this.drawFrame);
- }
-
- imageLoadError(event) {
- const image = event.target;
- console.log(`Error loading ${image.src}`);
- image.removeEventListener("load", this.imageLoaded);
- image.removeEventListener("error", this.imageLoadError);
- this.setState({message: `Error loading ${image.src}`});
- }
-
- setSelected(key) {
- if (this.loadTimer) {
- window.clearTimeout(this.loadTimer);
- this.loadTimer = null;
- }
-
- return window.fetch(`${base}/api/v1/games/${this.state.game.id}/player-selected/${key}`, {
- method: "PUT",
- cache: 'no-cache',
- credentials: 'same-origin',
- headers: {
- 'Content-Type': 'application/json'
- }
- }).then((res) => {
- if (res.status >= 400) {
- throw new Error(`Unable to set selected player!`);
- }
- return res.json();
- }).then((game) => {
- const error = (game.status !== 'success') ? game.status : undefined;
- this.updateGame(game);
- this.updateMessage();
- this.setState({ game: game, error: error });
- }).catch((error) => {
- console.error(error);
- this.setState({error: error.message});
- }).then(() => {
- this.resetGameLoad();
- window.requestAnimationFrame(this.drawFrame);
- });
- }
-
- setPlayerName(name) {
- if (this.loadTimer) {
- window.clearTimeout(this.loadTimer);
- this.loadTimer = null;
- }
-
- return window.fetch(`${base}/api/v1/games/${this.state.game.id}/player-name/${name}`, {
- method: "PUT",
- cache: 'no-cache',
- credentials: 'same-origin',
- headers: {
- 'Content-Type': 'application/json'
- }
- }).then((res) => {
- if (res.status >= 400) {
- throw new Error(`Unable to set player name!`);
- }
- return res.json();
- }).then((game) => {
- let message;
- if (game.status !== 'success') {
- message = game.status;
- } else {
- this.setState({ pickName: false });
- }
- this.updateGame(game);
- this.updateMessage();
-
- this.setState({ game: game, error: message});
- }).catch((error) => {
- console.error(error);
- this.setState({error: error.message});
- }).then(() => {
- this.resetGameLoad();
- window.requestAnimationFrame(this.drawFrame);
- });
- }
-
- shuffleBoard() {
- if (this.loadTimer) {
- window.clearTimeout(this.loadTimer);
- this.loadTimer = null;
- }
-
- return window.fetch(`${base}/api/v1/games/${this.state.game.id}/shuffle`, {
- method: "PUT",
- cache: 'no-cache',
- credentials: 'same-origin',
- headers: {
- 'Content-Type': 'application/json'
- }
- }).then((res) => {
- if (res.status >= 400) {
- throw new Error(`Unable to shuffle!`);
- }
- return res.json();
- }).then((game) => {
- console.log (`Board shuffled!`);
- this.updateGame(game);
- this.updateMessage();
- this.setState({ game: game, error: "Board shuffled!" });
- }).catch((error) => {
- console.error(error);
- this.setState({error: error.message});
- }).then(() => {
- this.resetGameLoad();
- window.requestAnimationFrame(this.drawFrame);
- });
- }
-
- rollDice() {
- if (this.loadTimer) {
- window.clearTimeout(this.loadTimer);
- this.loadTimer = null;
- }
-
- return window.fetch(`${base}/api/v1/games/${this.state.game.id}/roll`, {
- method: "PUT",
- cache: 'no-cache',
- credentials: 'same-origin',
- headers: {
- 'Content-Type': 'application/json'
- }
- }).then((res) => {
- if (res.status >= 400) {
- console.log(res);
- throw new Error(`Unable to roll dice`);
- }
- return res.json();
- }).then((game) => {
- const error = (game.status !== 'success') ? game.status : undefined;
- if (error) {
- game.dice = [ game.order ];
- }
- this.updateGame(game);
- this.updateMessage();
- this.setState({ game: { ...this.state.game, dice: game.dice }, error: error } );
- }).catch((error) => {
- console.error(error);
- this.setState({error: error.message});
- }).then(() => {
- this.resetGameLoad();
- return this.game.dice;
- });
- }
-
- loadGame() {
- if (this.loadTimer) {
- window.clearTimeout(this.loadTimer);
- this.loadTimer = null;
- }
-
- if (!this.state.game) {
- console.error('Attempting to loadGame with no game set');
+ if (!game) {
+ console.log("Nothing to render if there is no game!");
return;
}
- return window.fetch(`${base}/api/v1/games/${this.state.game.id}`, {
- method: "GET",
- cache: 'no-cache',
- credentials: 'same-origin',
- headers: {
- 'Content-Type': 'application/json'
- }
- }).then((res) => {
- if (res.status >= 400) {
- console.log(res);
- throw new Error(`Server temporarily unreachable.`);
- }
- return res.json();
- }).then((game) => {
- const error = (game.status !== 'success') ? game.status : undefined;
+ const canvas = canvasRef.current;
- //console.log (`Game ${game.id} loaded ${moment().format()}.`);
-
- this.updateGame(game);
- this.updateMessage();
- this.setState({ game: game, error: error });
- }).catch((error) => {
- console.error(error);
- this.setState({error: error.message});
- }).then(() => {
- this.resetGameLoad();
- });
- }
-
- resetGameLoad() {
- if (this.loadTimer) {
- window.clearTimeout(this.loadTimer);
- this.loadTimer = 0;
- }
- this.loadTimer = window.setTimeout(this.loadGame, 1000);
- }
-
- promoteGameState(change) {
- console.log("Requesting state change: ", change);
-
- if (this.loadTimer) {
- window.clearTimeout(this.loadTimer);
- this.loadTimer = null;
- }
-
- return window.fetch(`${base}/api/v1/games/${this.state.game.id}`, {
- method: "PUT",
- cache: 'no-cache',
- credentials: 'same-origin',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(change)
- }).then((res) => {
- if (res.status >= 400) {
- console.error(res);
- throw new Error(`Unable to change state`);
- }
- return res.json();
- }).then((game) => {
- this.updateGame(game);
- this.setState({ game: game, error: "" });
- }).catch((error) => {
- console.error(error);
- this.setState({error: error.message});
- }).then(() => {
- this.resetGameLoad();
- });
- }
-
- setGameState(state) {
- if (this.loadTimer) {
- window.clearTimeout(this.loadTimer);
- this.loadTimer = null;
- }
-
- return window.fetch(`${base}/api/v1/games/${this.state.game.id}/state/${state}`, {
- method: "PUT",
- cache: 'no-cache',
- credentials: 'same-origin',
- headers: {
- 'Content-Type': 'application/json'
- }
- }).then((res) => {
- if (res.status >= 400) {
- console.log(res);
- throw new Error(`Unable to set state to ${state}`);
- }
- return res.json();
- }).then((game) => {
- console.log (`Game state set to ${game.state}!`);
- this.updateGame(game);
- this.updateMessage();
- this.setState({ game: { ...this.state.game, state: game.state }, error: `Game state now ${game.state}.` });
- }).catch((error) => {
- console.error(error);
- this.setState({error: error.message});
- }).then(() => {
- this.resetGameLoad();
- return this.game.state;
- });
- }
-
- throwDice() {
- dice[0].pips = dice[1].pips = 0;
-
- return this.rollDice().then((roll) => {
- roll.forEach((value, index) => {
- dice[index] = {
- pips: value,
- angle: Math.random() * Math.PI * 2,
- jitter: (Math.random() - 0.5) * diceSize * 0.125
- };
- });
-
- window.requestAnimationFrame(this.drawFrame);
-
- if (this.game.state !== 'active') {
- return;
- }
-
- const sum = dice[0].pips + dice[1].pips;
- if (sum === 7) { /* Robber! */
- if (this.state.total > 7) {
- let half = Math.ceil(this.state.total * 0.5);
- this.setState({ total: this.state.total - half});
- while (half) {
- switch (Math.floor(Math.random() * 5)) {
- case 0: if (this.state.wood) { this.setState({ wood: this.state.wood - 1}); half--; } break;
- case 1: if (this.state.sheep) { this.setState({ sheep: this.state.sheep - 1}); half--; } break;
- case 2: if (this.state.stone) { this.setState({ stone: this.state.stone - 1}); half--; } break;
- case 3: if (this.state.brick) { this.setState({ brick: this.state.brick - 1}); half--; } break;
- case 4:
- default: if (this.state.wheat) { this.setState({ wheat: this.state.wheat - 1}); half--; } break;
- }
- }
- }
- } else {
- this.tiles.forEach((tile) => {
- if (tile.pip.roll !== sum) {
- return;
- }
- this.setState({ [tile.type]: this.state[tile.type] + 1});
- this.setState({ total: this.state.total + 1 });
- });
- }
-
- this.setState({
- total: this.state.total,
- wood: this.state.wood,
- sheep: this.state.sheep,
- stone: this.state.stone,
- brick: this.state.brick,
- wheat: this.state.wheat
- });
- }).catch((error) => {
- console.error(error);
- });
- }
-
- mouseMove(event) {
- const rect = this.canvas.parentElement.getBoundingClientRect();
- let x, y;
-
- if (event.changedTouches && event.changedTouches.length > 0) {
- x = event.changedTouches[0].clientX;
- y = event.changedTouches[0].clientY;
- } else {
- x = event.clientX;
- y = event.clientY;
- }
-
- if (this.offsetY) {
- y -= this.offsetY;
- }
-
- /* Scale mouse.x and mouse.y relative to board */
- this.mouse.x = (x - rect.left) /
- (this.minSize / hexagonRatio) - 0.5 - tileHeight * 0.5;
- this.mouse.y = (y - rect.top) /
- (this.minSize / hexagonRatio) - 0.5 - tileHeight * 0.5;
-
- /* Hide the mouse cursor circle after 0.5s */
- if (this.mouse.timer) {
- window.clearTimeout(this.mouse.timer);
- }
- this.mouse.timer = window.setTimeout(() => {
- this.mouse.timer = null;
- window.requestAnimationFrame(this.drawFrame);
- }, 500);
-
- let closest = null;
-
- this.tiles.forEach((tile) => {
- const dX = tile.pos.x - this.mouse.x,
- dY = tile.pos.y - this.mouse.y;
- const distance = Math.sqrt(dX * dX + dY * dY);
- if (distance > tileHeight * 0.75) {
- return;
- }
- if (!closest || closest.distance > distance) {
- closest = {
- tile: tile,
- distance: distance,
- angle: (distance !== 0.0) ? Math.atan2(dY, dX) : 0
- }
- }
- });
-
- if (!closest) {
- this.closest.tile = null;
- this.closest.info.distance = -1;
- this.closest.road = null;
- this.closest.angle = 0;
- this.closest.settlement = null;
- this.closest.tradeToken = null;
- } else {
- if (this.closest.tile !== closest.tile) {
- this.closest.tile = closest.tile;
- }
-
- this.closest.info.distance = closest.distance;
- this.closest.info.angle = closest.angle;
- }
-
- window.requestAnimationFrame(this.drawFrame);
- }
-
- updateDimensions() {
- const hasToolbar = false;
-
- if (this.updateSizeTimer) {
- clearTimeout(this.updateSizeTimer);
- }
-
- this.updateSizeTimer = setTimeout(() => {
- const container = document.getElementById("root"),
- offset = hasToolbar ? container.firstChild.offsetHeight : 0,
- height = window.innerHeight - offset;
-
- this.offsetY = offset;
- this.width = window.innerWidth;
- this.height = height;
-
- if (this.canvas) {
- this.canvas.width = this.width;
- this.canvas.height = this.height;
- this.canvas.style.top = `${offset}px`;
- this.canvas.style.width = `${this.width}px`;
- this.canvas.style.height = `${this.height}px`;
- }
-
- if (this.cards && this.cards.style) {
- this.cards.style.top = `${offset}px`;
- this.cards.style.width = `${this.width}px`;
- this.cards.style.height = `${this.heigh}tpx`;
- }
-
- this.updateSizeTimer = 0;
- this.drawFrame();
- }, 250);
- }
-
- drawFrame() {
- if (!this.canvas || !this.tabletop) {
+ if (canvas.width === 0 || canvas.height === 0) {
return;
}
- if (this.canvas.width === 0 || this.canvas.height === 0) {
- return;
- }
-
- const ctx = this.canvas.getContext("2d");
- ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ const ctx = canvas.getContext("2d");
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.strokeStyle = 'white';
ctx.fillStyle = 'rgba(0, 0, 0, 0)';
- this.minSize = Math.min(this.canvas.height, this.canvas.width);
+ minSize = Math.min(canvas.height, canvas.width);
/*
* Tabletop tiling:
* Image width: 1080
@@ -991,55 +160,55 @@ class Board extends React.Component {
*
* If the view is wider than taller, then
*/
- const tabletopLeft = 32 * this.tabletop.width / 1080,
- tabletopRight = 1010 * this.tabletop.width / 1080,
- tabletopLeaf = 978 * this.tabletop.width / 1080;
+ const tabletopLeft = 32 * tabletop.width / 1080,
+ tabletopRight = 1010 * tabletop.width / 1080,
+ tabletopLeaf = 978 * tabletop.width / 1080;
/* If view is taller than wide, tile the tabletop vertically */
ctx.save();
- if (this.canvas.height > this.canvas.width) {
- const tabletopHeight = this.canvas.width * this.tabletop.height / this.tabletop.width;
- for (let top = 0, step = 0; top < this.canvas.height; top += tabletopHeight, step++) {
+ if (canvas.height > canvas.width) {
+ const tabletopHeight = canvas.width * tabletop.height / tabletop.width;
+ for (let top = 0, step = 0; top < canvas.height; top += tabletopHeight, step++) {
if (step % 2) {
ctx.save();
ctx.translate(0, tabletopHeight - 1);
ctx.transform(1, 0, 0, -1, 0, 0);
- ctx.drawImage(this.tabletop,
+ ctx.drawImage(tabletop,
0, 0,
- this.tabletop.width, this.tabletop.height,
- 0, 0, this.canvas.width, this.canvas.width * this.tabletop.height / this.tabletop.width);
+ tabletop.width, tabletop.height,
+ 0, 0, canvas.width, canvas.width * tabletop.height / tabletop.width);
ctx.restore();
} else {
- ctx.drawImage(this.tabletop,
+ ctx.drawImage(tabletop,
0, 0,
- this.tabletop.width, this.tabletop.height,
+ tabletop.width, tabletop.height,
0, 0,
- this.canvas.width, this.canvas.width * this.tabletop.height / this.tabletop.width);
+ canvas.width, canvas.width * tabletop.height / tabletop.width);
}
ctx.translate(0, tabletopHeight);
}
} else {
- //const tabletopWidth = this.canvas.height * this.tabletop.width / this.tabletop.height;
- ctx.drawImage(this.tabletop,
+ //const tabletopWidth = canvas.height * tabletop.width / tabletop.height;
+ ctx.drawImage(tabletop,
0, 0,
- tabletopRight, this.tabletop.height,
+ tabletopRight, tabletop.height,
0, 0,
- this.canvas.height * tabletopRight / this.tabletop.height, this.canvas.height);
- let left = this.canvas.height * tabletopRight / this.tabletop.height;
- while (left < this.canvas.width) {
- ctx.drawImage(this.tabletop,
+ canvas.height * tabletopRight / tabletop.height, canvas.height);
+ let left = canvas.height * tabletopRight / tabletop.height;
+ while (left < canvas.width) {
+ ctx.drawImage(tabletop,
tabletopLeft, 0,
- tabletopLeaf, this.tabletop.height,
+ tabletopLeaf, tabletop.height,
left, 0,
- this.canvas.height * tabletopLeaf / this.tabletop.height, this.canvas.height);
- left += this.canvas.height * tabletopLeaf / this.tabletop.height;
+ canvas.height * tabletopLeaf / tabletop.height, canvas.height);
+ left += canvas.height * tabletopLeaf / tabletop.height;
}
}
ctx.restore();
- ctx.scale(this.minSize / hexagonRatio, this.minSize / hexagonRatio);
+ ctx.scale(minSize / hexagonRatio, minSize / hexagonRatio);
ctx.translate(0.5 * hexagonRatio, 0.5 * hexagonRatio);
- ctx.lineWidth = 2. / this.minSize;
+ ctx.lineWidth = 2. / minSize;
/* Board dimensions:
* ________
@@ -1055,19 +224,19 @@ class Board extends React.Component {
*/
ctx.save();
- this.drawBorders(ctx);
+ drawBorders(ctx);
ctx.restore();
ctx.save();
- this.drawPips(ctx);
+ drawPips(ctx);
ctx.restore();
ctx.fillStyle = "rgba(128, 128, 0, 0.125)";
ctx.strokeStyle = "rgba(255, 255, 0, 0.5)";
- if (this.game.state !== 'lobby') {
+ if (game.state !== 'lobby') {
const roll = dice[0].pips + dice[1].pips;
- if (roll) this.tiles.forEach((tile) => {
+ if (roll) tiles.forEach((tile) => {
if (tile.pip.roll === roll) {
ctx.save();
ctx.beginPath();
@@ -1079,12 +248,12 @@ class Board extends React.Component {
});
}
- if (this.closest.tile) {
+ if (closest.tile) {
ctx.save();
- Object.assign(ctx, getPlayerColors(this.game.color));
+ Object.assign(ctx, getPlayerColors(game.color));
- ctx.translate(this.closest.tile.pos.x, this.closest.tile.pos.y);
+ ctx.translate(closest.tile.pos.x, closest.tile.pos.y);
/* draw circle hovered current tile
ctx.beginPath();
ctx.arc(0, 0, tileHeight * 0.5, 0, Math.PI * 2.);
@@ -1092,7 +261,7 @@ class Board extends React.Component {
*/
/* road */
- let angle = Math.round(this.closest.info.angle / (Math.PI / 3.)) * (Math.PI / 3.);
+ let angle = Math.round(closest.info.angle / (Math.PI / 3.)) * (Math.PI / 3.);
ctx.rotate(angle);
ctx.translate(-tileHeight * 0.5, 0);
ctx.beginPath();
@@ -1103,7 +272,7 @@ class Board extends React.Component {
ctx.rotate(-angle);
/* village */
- angle = (this.closest.info.angle - Math.PI / 6.);
+ angle = (closest.info.angle - Math.PI / 6.);
angle = Math.round(angle / (Math.PI / 3.)) * (Math.PI / 3.);
angle += Math.PI / 6.;
ctx.rotate(angle);
@@ -1122,11 +291,11 @@ class Board extends React.Component {
/* For 0.5 after mouse movement, there is an on
* screen mouse helper. */
- if (this.mouse.timer) {
+ if (mouse.timer) {
ctx.strokeStyle = "rgba(0, 255, 255)";
ctx.fillStyle = "rgba(0, 255, 255, 0.25)";
ctx.beginPath();
- ctx.arc(this.mouse.x, this.mouse.y,
+ ctx.arc(mouse.x, mouse.y,
tileHeight * 0.5, 0, Math.PI * 2.);
ctx.stroke();
ctx.fill();
@@ -1138,19 +307,29 @@ class Board extends React.Component {
if (dice[0].pips) {
ctx.translate(-0.5 * (diceSize + diceMargin), 0);
- this.drawDie(ctx, dice[0]);
+ drawDie(ctx, dice[0]);
}
if (dice[1].pips) {
ctx.translate(diceSize + diceMargin, 0);
- this.drawDie(ctx, dice[1]);
+ drawDie(ctx, dice[1]);
}
ctx.restore();
ctx.restore();
- }
+ }, [ game ]);
- drawDie(ctx, die) {
+ const tiles = useMemo(() => Tiles(game, drawFrame), [ game, drawFrame]);
+ let offsetY = 0, width = 0;
+ let closest = {
+ info: {},
+ tile: null,
+ road: null,
+ tradeToken: null,
+ settlement: null
+ };
+
+ const drawDie = (ctx, die) => {
const radius = diceSize * 0.125,
offset = diceSize * 0.5 - radius,
pips = die.pips;
@@ -1228,8 +407,123 @@ class Board extends React.Component {
ctx.restore();
}
- drawPips(ctx) {
- const image = this.pips.image, pipSize = 0.06;
+ const mouseMove = useCallback((event) => {
+ const canvas = event.target,
+ rect = canvas.parentElement.getBoundingClientRect();
+ let x, y;
+
+ if (event.changedTouches && event.changedTouches.length > 0) {
+ x = event.changedTouches[0].clientX;
+ y = event.changedTouches[0].clientY;
+ } else {
+ x = event.clientX;
+ y = event.clientY;
+ }
+
+ if (offsetY) {
+ y -= offsetY;
+ }
+
+ /* Scale mouse.x and mouse.y relative to board */
+ mouse.x = (x - rect.left) /
+ (minSize / hexagonRatio) - 0.5 - tileHeight * 0.5;
+ mouse.y = (y - rect.top) /
+ (minSize / hexagonRatio) - 0.5 - tileHeight * 0.5;
+
+ /* Hide the mouse cursor circle after 0.5s */
+ if (mouse.timer) {
+ window.clearTimeout(mouse.timer);
+ }
+ mouse.timer = window.setTimeout(() => {
+ mouse.timer = null;
+ window.requestAnimationFrame(drawFrame);
+ }, 500);
+
+ let tmp = null;
+
+ tiles.forEach((tile) => {
+ const dX = tile.pos.x - mouse.x,
+ dY = tile.pos.y - mouse.y;
+ const distance = Math.sqrt(dX * dX + dY * dY);
+ if (distance > tileHeight * 0.75) {
+ return;
+ }
+ if (!tmp || tmp.distance > distance) {
+ tmp = {
+ tile: tile,
+ distance: distance,
+ angle: (distance !== 0.0) ? Math.atan2(dY, dX) : 0
+ }
+ }
+ });
+
+ if (!tmp) {
+ closest.tile = null;
+ closest.info.distance = -1;
+ closest.road = null;
+ closest.angle = 0;
+ closest.settlement = null;
+ closest.tradeToken = null;
+ } else {
+ closest.info.distance = closest.distance;
+ closest.info.angle = closest.angle;
+ }
+
+ window.requestAnimationFrame(drawFrame);
+ }, [ drawFrame, minSize, mouse, tiles ]);
+
+ const updateDimensions = useCallback(() => {
+ const hasToolbar = false;
+
+ if (updateSizeTimer) {
+ clearTimeout(updateSizeTimer);
+ }
+
+ const updateSizeTimer = setTimeout(() => {
+ const container = document.getElementById("root"),
+ offset = hasToolbar ? container.firstChild.offsetHeight : 0,
+ height = window.innerHeight - offset;
+
+ const canvas = canvasRef.current;
+
+ offsetY = offset;
+ width = window.innerWidth;
+ height = height;
+
+ if (canvas) {
+ canvas.width = width;
+ canvas.height = height;
+ canvas.style.top = `${offset}px`;
+ canvas.style.width = `${width}px`;
+ canvas.style.height = `${height}px`;
+ }
+
+ updateSizeTimer = 0;
+ window.requestAnimationFrame(drawFrame);
+ }, 250);
+ }, [ drawFrame, canvasRef ]);
+
+ useEffect(() => {
+ if (!canvasRef.current) {
+ return;
+ }
+
+ const canvas = canvasRef.current;
+
+ canvas.addEventListener('mousemove', mouseMove);
+ canvas.addEventListener('touchmove', mouseMove);
+ canvas.addEventListener('resize', updateDimensions);
+
+ return () => {
+ canvas.removeEventListener('mousemove', mouseMove);
+ canvas.removeEventListener('touchmove', mouseMove);
+ canvas.removeEventListener('resize', updateDimensions);
+
+ };
+ }, [ mouseMove, updateDimensions ]);
+
+ const drawPips = (ctx) => {
+ const image = pips.image, pipSize = 0.06;
function drawTile(tile, angle, radius) {
tile.pos.x = Math.sin(-angle) * radius;
@@ -1266,44 +560,44 @@ class Board extends React.Component {
}
let angle,
- radius = this.radius,
+ rotation = radius,
index = 0, pip; //, roll = dice[0].pips + dice[1].pips;
/* Outer row */
angle = 0;
for (let i = 0; i < 12; i++) {
angle -= Math.PI * 2. / 12.;
- pip = this.pips.pips[index++];
- this.tiles[i].pip = pip;
- drawTile(this.tiles[i], angle, radius - (i % 2) * 0.04);
- drawPip(pip, angle, radius - (i % 2) * 0.04, this.tiles[i].jitter);
+ pip = pips.pips[index++];
+ tiles[i].pip = pip;
+ drawTile(tiles[i], angle, rotation - (i % 2) * 0.04);
+ drawPip(pip, angle, rotation - (i % 2) * 0.04, tiles[i].jitter);
}
/* Middle row */
angle = Math.PI * 2. / 12.;
- radius = this.radius * 0.5;
+ rotation = radius * 0.5;
for (let i = 12; i < 18; i++) {
angle -= Math.PI * 2. / 6.;
- pip = this.pips.pips[index++];
- this.tiles[i].pip = pip;
- drawTile(this.tiles[i], angle, radius);
- drawPip(pip, angle, radius, this.tiles[i].jitter);
+ pip = pips.pips[index++];
+ tiles[i].pip = pip;
+ drawTile(tiles[i], angle, rotation);
+ drawPip(pip, angle, rotation, tiles[i].jitter);
}
/* Center */
let i = 18;
- pip = this.pips.pips[index++];
- this.tiles[i].pip = pip;
- drawTile(this.tiles[i], 0, 0);
- drawPip(pip, 0, 0, this.tiles[i].jitter);
+ pip = pips.pips[index++];
+ tiles[i].pip = pip;
+ drawTile(tiles[i], 0, 0);
+ drawPip(pip, 0, 0, tiles[i].jitter);
}
- drawBorders(ctx) {
+ const drawBorders = (ctx) => {
ctx.rotate(Math.PI);
const offset = 0.18;
- this.borders.forEach((border, index) => {
- ctx.translate(0, this.radius);
+ borders.forEach((border, index) => {
+ ctx.translate(0, radius);
const image = border.image;
@@ -1311,238 +605,40 @@ class Board extends React.Component {
-offset, 0,
0.5, 0.5 * image.height / image.width);
- ctx.translate(0, -this.radius);
+ ctx.translate(0, -radius);
ctx.rotate(Math.PI * 2. / 6.);
});
}
- updateGame(game) {
- this.game = game;
-
- if (game.state === "invalid") {
+ const updateGame = (game) => {
+ if (!game || game.state === "invalid") {
return;
}
- this.pips = Pips(this);
- this.tiles = Tiles(this);
- this.tabletop = Tabletop(this);
-
- this.borders = this.game.borders.map((file) => {
- return Border(this, file);
+ setPips({
+ image: loadImage('pip-numbers.png', drawFrame),
+ pips: game.pips
});
-
- window.requestAnimationFrame(this.drawFrame);
+ setTabletop(loadImage('tabletop.png', drawFrame));
+ setBorders(game.borders.map((border) => {
+ return {
+ image: loadImage(border.file, drawFrame)
+ };
+ }));
}
- updateMessage() {
- const player = (this.game && this.game.color) ? this.game.players[this.game.color] : undefined,
- name = this.game ? this.game.name : "";
-
- let message = <>>;
- if (this.state.pickName || !name) {
- message = <>{message}Enter the name you would like to be known by, then press ENTER or select SET.>;
- } else {
- switch (this.game && this.game.state) {
- case 'lobby':
- message = <>{message}You are in the lobby as {name}.>;
- if (!this.game.color) {
- message = <>{message}You need to pick your color.>;
- } else {
- message = <>{message}You have selected .>;
- }
- message = <>{message}You can chat with other players below.>;
- if (this.game.active < 2) {
- message = <>{message}Once there are two or more players, you can select .>;
- } else {
- message = <>{message}There are enough players to start the game. Select when ready.>;
- }
- break;
- case 'game-order':
- if (!player) {
- message = <>{message}This game as an observer as {name}.>;
- message = <>{message}You can chat with other players below as {this.game.name}, but cannot play unless players go back to the Lobby.>;
- } else {
- if (!player.order) {
- message = <>{message}You need to roll for game order. Click Roll Dice below.>;
- } else {
- message = <>{message}You rolled for game order. Waiting for all players to roll.>;
- message = <>{message}
THIS IS THE END OF THE FUNCTIONALITY SO FAR>;
- }
- }
- break;
- case 'active':
- if (!player) {
- message = <>{message}This game is no longer in the lobby.
TODO: Override game state to allow Lobby mode while in-game>;
- } else {
- message = <>{message}
THIS IS THE END OF THE FUNCTIONALITY SO FAR>;
- }
- break;
- case null:
- case undefined:
- case '':
- message = <>{message}The game is in a wonky state. Sorry :(>;
- break;
- default:
- message = <>{message}Game state is: {this.game.state}>;
- break;
- }
- }
- this.setState({ message: message });
+ if (!game) {
+ console.log("Game not set with initial board. Rendering nothing.");
+ return <>>;
}
- componentDidMount() {
- this.start = new Date();
+ return (
+
+ );
+};
- window.addEventListener("touchmove", this.mouseMove);
- window.addEventListener("mousemove", this.mouseMove);
- window.addEventListener("resize", this.updateDimensions);
-
- const params = {};
- if (this.id) {
- console.log(`Loading game: ${this.id}`);
- params.url = `${base}/api/v1/games/${this.id}`;
- params.method = "GET"
- } else {
- console.log("Requesting new game.");
- params.url = `${base}/api/v1/games/`;
- params.method = "POST";
- }
-
- window.fetch(params.url, {
- method: params.method,
- cache: 'no-cache',
- credentials: 'same-origin',
- headers: {
- 'Content-Type': 'application/json'
- },
-// body: JSON.stringify(data) // body data type must match "Content-Type" header
- }).then((res) => {
- if (res.status < 400) {
- return res;
- }
- let error;
- if (!this.id) {
- error = `Unable to create new game.`;
- throw new Error(error);
- }
-
- error = `Unable to find game ${this.id}. Starting new game.`
- console.log(error);
- this.setState({ error: error });
-
- params.url = `${base}/api/v1/games/${this.id}`;
- params.method = "POST";
-
- return window.fetch(params.url, {
- method: params.method,
- cache: 'no-cache',
- credentials: 'same-origin',
- headers: {
- 'Content-Type': 'application/json'
- }
- });
- }).then((res) => {
- return res.json();
- }).then((game) => {
-// console.log (`Game ${game.id} loaded ${moment().format()}.`);
-
- if (!this.id) {
- history.push(`${gamesPath}/${game.id}`);
- }
-
- this.updateGame(game);
- this.updateMessage();
-
- this.setState({ game: game, error: "" });
- }).catch((error) => {
- console.error(error);
- this.setState({error: error.message});
- }).then(() => {
- this.resetGameLoad();
- });
-
- setTimeout(this.updateDimensions, 1000);
- }
-
- componentWillUnmount() {
- if (this.loadTimer) {
- clearTimeout(this.loadTimer);
- }
- if (this.updateSizeTimer) {
- clearTimeout(this.updateSizeTimer);
- this.updateSizeTimer = 0;
- }
-
- window.removeEventListener("mousemove", this.mouseMove);
- window.removeEventListener("touchmove", this.mouseMove);
- window.removeEventListener("resize", this.updateDimensions);
- }
-
- render() {
- const game = this.state.game;
- return (
- this.el = el}>
-
-
- { game &&
-
{ this.state.message }
- {(this.state.pickName || !this.game.name) &&
}
- {(!this.state.pickName && this.game.name) && <>
-
-
-
- > }
- }
-
-
this.cards = el}>
- { game && game.state === "active" && <>
-
In hand
-
-
-
-
-
-
-
-
Available to play
-
-
Points
-
-
-
-
Stats
-
-
Points: 7
-
Cards: {this.state.total}
-
Roads remaining: 4
-
Longest road: 7
-
Cities remaining: 4
-
Settlements remaining: 5
-
-
- > }
-
-
- { this.state.error &&
{this.state.error}
}
-
-
- );
- }
-}
-export default withRouter(props => );
+export default Board;
diff --git a/client/src/Table.css b/client/src/Table.css
new file mode 100755
index 0000000..8654e62
--- /dev/null
+++ b/client/src/Table.css
@@ -0,0 +1,339 @@
+.Table {
+ display: flex;
+ position: absolute;
+ width: 100%;
+ overflow: hidden;
+ height: 100%;
+ justify-content: right;
+}
+
+.Display {
+ display: inline-block;
+ position: absolute;
+}
+
+.PlayerColor {
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ width: 1em;
+ height: 1em;
+ padding: 0.125em;
+ margin: 0 0.25em;
+ border-radius: 0.625em;
+ border-width: 1px;
+ border-style: solid;
+ text-align: center;
+}
+
+.PlayerColor > div {
+ font-weight: bold;
+ overflow: hidden;
+ font-size: 0.75rem;
+}
+
+.Cards {
+ display: inline-block;
+ position: absolute;
+ text-align: right;
+ vertical-align: bottom;
+ padding: 0.5em;
+ box-sizing: border-box;
+ max-height: 100%;
+ max-width: 100%;
+ opacity: 0.7;
+}
+
+.Stack {
+ position: relative;
+ display: inline-block;
+}
+
+.Stack:not(:first-child) {
+ margin-left: -3em;
+ transition: margin-left 1s ease-in-out 0.25s;
+}
+
+.Stack > * {
+ transition: margin-left 1s ease-in-out 0.25s, margin-right 1s ease-in-out 0.25s;
+}
+
+.Development:hover,
+.Placard:hover,
+.Resource:hover {
+ filter: brightness(150%);
+}
+
+.Dice {
+ width: 1rem;
+ height: 1rem;
+ background-color: black;
+}
+
+.Game {
+ display: inline-flex;
+ flex-direction: column;
+ box-sizing: border-box;
+ width: 40vw;
+ max-height: 100vh;
+ overflow: hidden;
+ max-width: 40vw;
+ z-index: 100;
+ padding: 0.5em;
+ opacity: 0.7;
+}
+
+.Game > * {
+ /* for Firefox */
+ min-height: 0;
+}
+
+.Game > *:not(:last-child) {
+ margin-bottom: 0.5em;
+}
+
+.Game .lobby {
+ width: 100vw;
+}
+
+.Chat {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ padding: 0.5em;
+}
+
+.Chat > * {
+ /* for Firefox */
+ min-height: 0;
+}
+
+#ChatList {
+ flex: 1;
+ overflow: auto;
+ scroll-behavior: smooth;
+ align-items: flex-start;
+}
+
+#ChatList .MuiListItem-gutters {
+ padding: 2px 0 2px 0;
+}
+
+#ChatList .MuiTypography-body1 {
+ font-size: 0.8rem;
+}
+
+#ChatList .MuiTypography-body2 {
+ font-size: 0.7rem;
+}
+
+#ChatList .MuiListItemText-multiline {
+ margin-top: 0;
+ margin-bottom: 0;
+ padding: 4px 0px 4px 4px;
+}
+
+#ChatList .PlayerColor {
+ width: 1em;
+ height: 1em;
+ padding: 0;
+ margin-top: 4px;
+}
+
+.Players {
+ padding: 0.5em;
+ user-select: none;
+}
+
+.PlayerSelector .PlayerColor {
+ width: 1em;
+ height: 1em;
+}
+
+.PlayerSelector {
+ display: inline-flex;
+ flex-wrap: wrap;
+ flex-direction: row;
+ justify-content: space-between;
+}
+
+.PlayerSelector.MuiList-padding {
+ padding: 0;
+}
+
+.PlayerSelector .PlayerEntry {
+ flex: 1 1 0px;
+ align-items: center;
+ display: inline-flex;
+ flex-direction: row;
+ min-width: 10em;
+}
+
+.PlayerSelector .MuiTypography-body1 {
+ font-size: 0.8rem;
+ white-space: nowrap;
+}
+
+.PlayerSelector .MuiTypography-body2 {
+ font-size: 0.7rem;
+ white-space: nowrap;
+}
+
+.Players .PlayerEntry {
+ border: 1px solid rgba(0,0,0,0);
+ border-radius: 0.5em;
+ min-width: 10em;
+}
+
+.Players .PlayerEntry[data-selectable=true]:hover {
+ border-color: rgba(0,0,0,0.5);
+ cursor: pointer;
+}
+
+.Players .PlayerEntry[data-selected=true] {
+ background-color: rgba(255, 255, 0, 0.5);
+}
+
+.Players .PlayerToggle {
+ min-width: 5em;
+ display: inline-flex;
+ align-items: flex-end;
+ flex-direction: column;
+}
+
+.PlayerName {
+ padding: 0.5em;
+}
+
+.Players > * {
+ width: 100%;
+}
+
+.Players .nameInput {
+ flex-grow: 1;
+}
+
+.Stack > *:not(:first-child) {
+ margin-left: -4.5em;
+}
+
+.Hand {
+ min-height: calc(7.2em + 0.5em);
+}
+
+.Hand:hover .Stack:hover > *:not(:first-child) {
+ margin-left: -2em;
+}
+
+.Hand:hover .Stack:hover:not(:last-child) > *:last-child {
+ margin-right: 3em;
+}
+
+.Placard {
+ position: relative;
+ width: 9.4em;
+ height: 11.44em;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: cover;
+ margin: 0.25em;
+ display: inline-block;
+}
+
+.Development {
+ position: relative;
+ display: inline-block;
+ width: 4.9em;
+ height: 7.2em;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: cover;
+ margin: 0.25em;
+}
+
+.Resource {
+ position: relative;
+ width: 4.9em;
+ height: 7.2em;
+ display: inline-block;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: cover;
+ margin: 0.25em;
+}
+
+.Action {
+ display: flex;
+ align-items: center;
+ justify-content: space-evenly;
+ background-color: rgba(16, 16, 16, 0.25);
+ padding: 0.25em;
+}
+
+button {
+ margin: 0.25em;
+ background-color: white;
+ border: 1px solid black !important;
+}
+
+.Error {
+ display: flex;
+ position: absolute;
+ top: calc(50vh - 1.5em);
+ left: 0px;
+ right: 0px;
+ align-items: center;
+ justify-content: center;
+ background-color: yellow;
+ text-align: left;
+ font-size: 12pt;
+ padding: 1em;
+ margin: 1em;
+ z-index: 10000;
+}
+
+.Message {
+ display: inline;
+ justify-content: left;
+ background-color: rgba(224, 224, 224);
+ text-align: left;
+ font-size: 12pt;
+ padding: 0.5em;
+ user-select: none;
+}
+
+.Message .PlayerColor {
+ width: 1em;
+ height: 1em;
+}
+
+.Message div {
+ display: inline-flex;
+}
+
+.PlayerName {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: row;
+}
+
+.PlayerName > .nameInput {
+ margin-right: 1em;
+ flex: 1;
+ max-width: 30em;
+}
+
+.PlayerName > Button {
+ background: lightblue;
+}
+
+.Statistics > div:nth-child(2) {
+ display: flex;
+ flex-direction: row;
+ border: 1px solid black;
+}
+
+.Statistics div:nth-child(2) div {
+ padding: 0.25em 0.5em;
+}
diff --git a/client/src/Table.js b/client/src/Table.js
new file mode 100755
index 0000000..35e2ea4
--- /dev/null
+++ b/client/src/Table.js
@@ -0,0 +1,1026 @@
+import React, { useState, useEffect } from "react";
+import "./Table.css";
+import history from "./history.js";
+import Paper from '@material-ui/core/Paper';
+import Button from '@material-ui/core/Button';
+import TextField from '@material-ui/core/TextField';
+import List from '@material-ui/core/List';
+import ListItem from '@material-ui/core/ListItem';
+import ListItemText from '@material-ui/core/ListItemText';
+import { makeStyles } from '@material-ui/core/styles';
+import { orange,lightBlue, red, grey } from '@material-ui/core/colors';
+import Avatar from '@material-ui/core/Avatar';
+import Moment from 'react-moment';
+import Board from './Board.js'
+
+//import moment from 'moment';
+
+/* Start of withRouter polyfill */
+// https://reactrouter.com/docs/en/v6/faq#what-happened-to-withrouter-i-need-it
+import {
+ useLocation,
+ useNavigate,
+ useParams
+} from "react-router-dom";
+
+function withRouter(Component) {
+ function ComponentWithRouterProp(props) {
+ let location = useLocation();
+ let navigate = useNavigate();
+ let params = useParams();
+ return (
+
+ );
+ }
+
+ return ComponentWithRouterProp;
+}
+/* end of withRouter polyfill */
+
+const base = process.env.PUBLIC_URL;
+
+const assetsPath = `${base}/assets`;
+const gamesPath = `${base}/games`;
+
+const useStyles = makeStyles((theme) => ({
+ root: {
+ display: 'flex',
+ '& > *': {
+ margin: theme.spacing(1),
+ },
+ },
+ R: {
+ color: theme.palette.getContrastText(red[500]),
+ backgroundColor: red[500],
+ },
+ O: {
+ color: theme.palette.getContrastText(orange[500]),
+ backgroundColor: orange[500],
+ },
+ W: {
+ color: theme.palette.getContrastText(grey[500]),
+ backgroundColor: grey[500],
+ },
+ B: {
+ color: theme.palette.getContrastText(lightBlue[500]),
+ backgroundColor: lightBlue[500],
+ },
+}));
+
+const Dice = ({ pips }) => {
+ let name;
+ switch (pips) {
+ case 1: name = 'one'; break;
+ case 2: name = 'two'; break;
+ case 3: name = 'three'; break;
+ case 4: name = 'four'; break;
+ case 5: name = 'five'; break;
+ default:
+ case 6: name = 'six'; break;
+ }
+ return (
+
+ );
+}
+
+const PlayerColor = ({ color }) => {
+ const classes = useStyles();
+ return (
+
+ );
+};
+
+const diceSize = 0.05,
+ dice = [ {
+ pips: 0,
+ jitter: 0,
+ angle: 0
+ }, {
+ pips: 0,
+ jitter: 0,
+ angle: 0
+ } ];
+
+
+class Placard extends React.Component {
+ render() {
+ return (
+
+
+ );
+ }
+};
+
+class Development extends React.Component {
+ render() {
+ const array = [];
+ for (let i = 0; i < this.props.count; i++) {
+ if (this.props.type.match(/-$/)) {
+ array.push(i + 1);//Math.ceil(Math.random() * this.props.max));
+ } else {
+ array.push("");
+ }
+ }
+ return (
+
+ { React.Children.map(array, i => (
+
+
+ )) }
+
+ );
+ }
+};
+
+class Resource extends React.Component {
+ render() {
+ const array = new Array(Number(this.props.count ? this.props.count : 0));
+ return (
+ <>
+ { array.length > 0 &&
+
+ { React.Children.map(array, i => (
+
+
+ )) }
+
+ }
+ >
+ );
+ }
+};
+
+const Chat = ({ table, promoteGameState }) => {
+ const [lastTop, setLastTop] = useState(0),
+ [autoScroll, setAutoscroll] = useState(true),
+ [scrollTime, setScrollTime] = useState(0);
+
+ const chatInput = (event) => {
+ };
+
+ const chatKeyPress = (event) => {
+ if (event.key === "Enter") {
+ if (!autoScroll) {
+ setAutoscroll(true);
+ }
+
+ promoteGameState({
+ chat: {
+ player: table.game.color ? table.game.color : undefined,
+ message: event.target.value
+ }
+ });
+ event.target.value = "";
+ }
+ };
+
+ const chatScroll = (event) => {
+ const chatList = event.target,
+ fromBottom = Math.round(Math.abs((chatList.scrollHeight - chatList.offsetHeight) - chatList.scrollTop));
+
+ /* If scroll is within 20 pixels of the bottom, turn on auto-scroll */
+ const shouldAutoscroll = (fromBottom < 20);
+
+ if (shouldAutoscroll !== autoScroll) {
+ setAutoscroll(shouldAutoscroll);
+ }
+
+ /* If the list should not auto scroll, then cache the current
+ * top of the list and record when we did this so we honor
+ * the auto-scroll for at least 500ms */
+ if (!shouldAutoscroll) {
+ const target = Math.round(chatList.scrollTop);
+ if (target !== lastTop) {
+ setLastTop(target);
+ setScrollTime(Date.now());
+ }
+ }
+ };
+
+ useEffect(() => {
+ const chatList = document.getElementById("ChatList"),
+ currentTop = Math.round(chatList.scrollTop);
+
+ if (autoScroll) {
+ /* Auto-scroll to the bottom of the chat window */
+ const target = Math.round(chatList.scrollHeight - chatList.offsetHeight);
+ if (currentTop !== target) {
+ chatList.scrollTop = target;
+ }
+ return;
+ }
+
+ /* Maintain current position in scrolled view if the user hasn't
+ * been scrolling in the past 0.5s */
+ if ((Date.now() - scrollTime) > 500 && currentTop !== lastTop) {
+ chatList.scrollTop = lastTop;
+ }
+ });
+
+ //const timeDelta = game.timestamp - Date.now();
+ if (!table.game) {
+ console.log("Why no game?");
+ }
+
+ const messages = table.game && table.game.chat.map((item, index) => {
+ /* If the date is in the future, set it to now */
+ const name = getPlayerName(table.game.sessions, item.from),
+ from = name ? `${name}, ` : '';
+ return (
+
+
+ {from}
+ Date.now() ?
+ Date.now() : item.date} interval={1000}/>>)} />
+
+ );
+ });
+
+ const name = table.game ? table.game.name : "Why no game?";
+
+ return (
+
+
+ { messages }
+
+ )} variant="outlined"/>
+
+ );
+}
+
+const StartButton = ({ table }) => {
+ const startClick = (event) => {
+ table.setGameState("game-order").then((state) => {
+ table.game.state = state;
+ });
+ };
+
+ return (
+
+ );
+};
+
+const Action = ({ table }) => {
+ const newTableClick = (event) => {
+ return table.shuffleTable();
+ };
+
+ const rollClick = (event) => {
+ table.throwDice();
+ }
+
+ const passClick = (event) => {
+ }
+
+ if (!table.game) {
+ console.log("Why no game?");
+ return ();
+ }
+
+ return (
+
+ { table.game.state === 'lobby' && <>
+
+
+ > }
+ { table.game.state === 'game-order' &&
+ }
+ { table.game.state === 'active' && <>
+
+
+ > }
+
+ );
+}
+
+const PlayerName = ({table}) => {
+ const [name, setName] = useState((table && table.game && table.game.name) ? table.game.name : "");
+
+ const nameChange = (event) => {
+ setName(event.target.value);
+ }
+
+ const sendName = () => {
+ console.log(`Send: ${name}`);
+ if (name !== table.game.name) {
+ table.setPlayerName(name);
+ } else {
+ table.setState({ pickName: false, error: "" });
+ }
+ }
+
+ const nameKeyPress = (event) => {
+ if (event.key === "Enter") {
+ sendName();
+ }
+ }
+
+ return (
+
+
+
+
+ );
+};
+
+const getPlayerName = (sessions, color) => {
+ for (let i = 0; i < sessions.length; i++) {
+ const session = sessions[i];
+ if (session.color === color) {
+ return session.name;
+ }
+ }
+ return null;
+}
+
+/* This needs to take in a mechanism to declare the
+ * player's active item in the game */
+const Players = ({ table }) => {
+ const toggleSelected = (key) => {
+ console.log('toggle');
+ table.setSelected(table.game.color === key ? "" : key);
+ }
+
+ const players = [];
+
+ if (!table.game) {
+ console.log("Why no game?");
+ for (let color in table.game.players) {
+ const item = table.game.players[color], inLobby = table.game.state === 'lobby';
+ if (!inLobby && item.status === 'Not active') {
+ continue;
+ }
+ const name = getPlayerName(table.game.sessions, color),
+ selectable = table.game.state === 'lobby' && (item.status === 'Not active' || table.game.color === color);
+ let toggleText = name ? name : "Available";
+ players.push((
+ { inLobby && selectable && toggleSelected(color) }}
+ key={`player-${color}`}>
+
+
+ { item.status + ' ' }
+ { item.status !== 'Not active' && Date.now() ? Date.now() : item.lastActive}/>}
+ >)} />
+ { !inLobby && table.game.color === color &&
+
+ }
+
+ ));
+ }
+ }
+
+ return (
+
+
+ { players }
+
+
+ );
+}
+
+console.log("TODO: Convert this to a function component!!!!");
+
+class Table extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ total: 0,
+ wood: 0,
+ sheep: 0,
+ brick: 0,
+ stone: 0,
+ wheat: 0,
+ game: null,
+ message: "",
+ error: ""
+ };
+ this.componentDidMount = this.componentDidMount.bind(this);
+ this.updateDimensions = this.updateDimensions.bind(this);
+ this.throwDice = this.throwDice.bind(this);
+ this.promoteGameState = this.promoteGameState.bind(this);
+ this.resetGameLoad = this.resetGameLoad.bind(this);
+ this.loadGame = this.loadGame.bind(this);
+ this.rollDice = this.rollDice.bind(this);
+ this.setGameState = this.setGameState.bind(this);
+ this.shuffleTable = this.shuffleTable.bind(this);
+ this.updateGame = this.updateGame.bind(this);
+ this.imageLoadError = this.imageLoadError.bind(this);
+ this.imageLoaded = this.imageLoaded.bind(this);
+ this.setPlayerName = this.setPlayerName.bind(this);
+ this.setSelected = this.setSelected.bind(this);
+ this.updateMessage = this.updateMessage.bind(this);
+
+ this.mouse = { x: 0, y: 0 };
+ this.radius = 0.317;
+
+ this.loadTimer = null;
+
+ this.game = null;
+ this.pips = [];
+ this.tiles = [];
+ this.borders = [];
+ this.tabletop = null;
+ this.closest = {
+ info: {},
+ tile: null,
+ road: null,
+ tradeToken: null,
+ settlement: null
+ };
+
+ this.id = (props.router && props.router.params.id) ? props.router.params.id : 0;
+ }
+
+ imageLoaded(event) {
+ const image = event.target;
+ console.log(`Done loading ${image.src}`);
+ image.removeEventListener("load", this.imageLoaded);
+ image.removeEventListener("error", this.imageLoadError);
+ window.requestAnimationFrame(this.drawFrame);
+ }
+
+ imageLoadError(event) {
+ const image = event.target;
+ console.log(`Error loading ${image.src}`);
+ image.removeEventListener("load", this.imageLoaded);
+ image.removeEventListener("error", this.imageLoadError);
+ this.setState({message: `Error loading ${image.src}`});
+ }
+
+ setSelected(key) {
+ if (this.loadTimer) {
+ window.clearTimeout(this.loadTimer);
+ this.loadTimer = null;
+ }
+
+ return window.fetch(`${base}/api/v1/games/${this.state.game.id}/player-selected/${key}`, {
+ method: "PUT",
+ cache: 'no-cache',
+ credentials: 'same-origin',
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }).then((res) => {
+ if (res.status >= 400) {
+ throw new Error(`Unable to set selected player!`);
+ }
+ return res.json();
+ }).then((game) => {
+ const error = (game.status !== 'success') ? game.status : undefined;
+ this.updateGame(game);
+ this.updateMessage();
+ this.setState({ game: game, error: error });
+ }).catch((error) => {
+ console.error(error);
+ this.setState({error: error.message});
+ }).then(() => {
+ this.resetGameLoad();
+ window.requestAnimationFrame(this.drawFrame);
+ });
+ }
+
+ setPlayerName(name) {
+ if (this.loadTimer) {
+ window.clearTimeout(this.loadTimer);
+ this.loadTimer = null;
+ }
+
+ return window.fetch(`${base}/api/v1/games/${this.state.game.id}/player-name/${name}`, {
+ method: "PUT",
+ cache: 'no-cache',
+ credentials: 'same-origin',
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }).then((res) => {
+ if (res.status >= 400) {
+ throw new Error(`Unable to set player name!`);
+ }
+ return res.json();
+ }).then((game) => {
+ let message;
+ if (game.status !== 'success') {
+ message = game.status;
+ } else {
+ this.setState({ pickName: false });
+ }
+ this.updateGame(game);
+ this.updateMessage();
+
+ this.setState({ game: game, error: message});
+ }).catch((error) => {
+ console.error(error);
+ this.setState({error: error.message});
+ }).then(() => {
+ this.resetGameLoad();
+ window.requestAnimationFrame(this.drawFrame);
+ });
+ }
+
+ shuffleTable() {
+ if (this.loadTimer) {
+ window.clearTimeout(this.loadTimer);
+ this.loadTimer = null;
+ }
+
+ return window.fetch(`${base}/api/v1/games/${this.state.game.id}/shuffle`, {
+ method: "PUT",
+ cache: 'no-cache',
+ credentials: 'same-origin',
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }).then((res) => {
+ if (res.status >= 400) {
+ throw new Error(`Unable to shuffle!`);
+ }
+ return res.json();
+ }).then((game) => {
+ console.log (`Table shuffled!`);
+ this.updateGame(game);
+ this.updateMessage();
+ this.setState({ game: game, error: "Table shuffled!" });
+ }).catch((error) => {
+ console.error(error);
+ this.setState({error: error.message});
+ }).then(() => {
+ this.resetGameLoad();
+ window.requestAnimationFrame(this.drawFrame);
+ });
+ }
+
+ rollDice() {
+ if (this.loadTimer) {
+ window.clearTimeout(this.loadTimer);
+ this.loadTimer = null;
+ }
+
+ return window.fetch(`${base}/api/v1/games/${this.state.game.id}/roll`, {
+ method: "PUT",
+ cache: 'no-cache',
+ credentials: 'same-origin',
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }).then((res) => {
+ if (res.status >= 400) {
+ console.log(res);
+ throw new Error(`Unable to roll dice`);
+ }
+ return res.json();
+ }).then((game) => {
+ const error = (game.status !== 'success') ? game.status : undefined;
+ if (error) {
+ game.dice = [ game.order ];
+ }
+ this.updateGame(game);
+ this.updateMessage();
+ this.setState({ game: { ...this.state.game, dice: game.dice }, error: error } );
+ }).catch((error) => {
+ console.error(error);
+ this.setState({error: error.message});
+ }).then(() => {
+ this.resetGameLoad();
+ return this.game.dice;
+ });
+ }
+
+ loadGame() {
+ if (this.loadTimer) {
+ window.clearTimeout(this.loadTimer);
+ this.loadTimer = null;
+ }
+
+ if (!this.state.game) {
+ console.error('Attempting to loadGame with no game set');
+ return;
+ }
+
+ return window.fetch(`${base}/api/v1/games/${this.state.game.id}`, {
+ method: "GET",
+ cache: 'no-cache',
+ credentials: 'same-origin',
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }).then((res) => {
+ if (res.status >= 400) {
+ console.log(res);
+ throw new Error(`Server temporarily unreachable.`);
+ }
+ return res.json();
+ }).then((game) => {
+ const error = (game.status !== 'success') ? game.status : undefined;
+
+ //console.log (`Game ${game.id} loaded ${moment().format()}.`);
+
+ this.updateGame(game);
+ this.updateMessage();
+ this.setState({ game: game, error: error });
+ }).catch((error) => {
+ console.error(error);
+ this.setState({error: error.message});
+ }).then(() => {
+ this.resetGameLoad();
+ });
+ }
+
+ resetGameLoad() {
+ if (this.loadTimer) {
+ window.clearTimeout(this.loadTimer);
+ this.loadTimer = 0;
+ }
+ this.loadTimer = window.setTimeout(this.loadGame, 1000);
+ }
+
+ promoteGameState(change) {
+ console.log("Requesting state change: ", change);
+
+ if (this.loadTimer) {
+ window.clearTimeout(this.loadTimer);
+ this.loadTimer = null;
+ }
+
+ return window.fetch(`${base}/api/v1/games/${this.state.game.id}`, {
+ method: "PUT",
+ cache: 'no-cache',
+ credentials: 'same-origin',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(change)
+ }).then((res) => {
+ if (res.status >= 400) {
+ console.error(res);
+ throw new Error(`Unable to change state`);
+ }
+ return res.json();
+ }).then((game) => {
+ this.updateGame(game);
+ this.setState({ game: game, error: "" });
+ }).catch((error) => {
+ console.error(error);
+ this.setState({error: error.message});
+ }).then(() => {
+ this.resetGameLoad();
+ });
+ }
+
+ setGameState(state) {
+ if (this.loadTimer) {
+ window.clearTimeout(this.loadTimer);
+ this.loadTimer = null;
+ }
+
+ return window.fetch(`${base}/api/v1/games/${this.state.game.id}/state/${state}`, {
+ method: "PUT",
+ cache: 'no-cache',
+ credentials: 'same-origin',
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }).then((res) => {
+ if (res.status >= 400) {
+ console.log(res);
+ throw new Error(`Unable to set state to ${state}`);
+ }
+ return res.json();
+ }).then((game) => {
+ console.log (`Game state set to ${game.state}!`);
+ this.updateGame(game);
+ this.updateMessage();
+ this.setState({ game: { ...this.state.game, state: game.state }, error: `Game state now ${game.state}.` });
+ }).catch((error) => {
+ console.error(error);
+ this.setState({error: error.message});
+ }).then(() => {
+ this.resetGameLoad();
+ return this.game.state;
+ });
+ }
+
+ throwDice() {
+ dice[0].pips = dice[1].pips = 0;
+
+ return this.rollDice().then((roll) => {
+ roll.forEach((value, index) => {
+ dice[index] = {
+ pips: value,
+ angle: Math.random() * Math.PI * 2,
+ jitter: (Math.random() - 0.5) * diceSize * 0.125
+ };
+ });
+
+ window.requestAnimationFrame(this.drawFrame);
+
+ if (this.game.state !== 'active') {
+ return;
+ }
+
+ const sum = dice[0].pips + dice[1].pips;
+ if (sum === 7) { /* Robber! */
+ if (this.state.total > 7) {
+ let half = Math.ceil(this.state.total * 0.5);
+ this.setState({ total: this.state.total - half});
+ while (half) {
+ switch (Math.floor(Math.random() * 5)) {
+ case 0: if (this.state.wood) { this.setState({ wood: this.state.wood - 1}); half--; } break;
+ case 1: if (this.state.sheep) { this.setState({ sheep: this.state.sheep - 1}); half--; } break;
+ case 2: if (this.state.stone) { this.setState({ stone: this.state.stone - 1}); half--; } break;
+ case 3: if (this.state.brick) { this.setState({ brick: this.state.brick - 1}); half--; } break;
+ case 4:
+ default: if (this.state.wheat) { this.setState({ wheat: this.state.wheat - 1}); half--; } break;
+ }
+ }
+ }
+ } else {
+ this.tiles.forEach((tile) => {
+ if (tile.pip.roll !== sum) {
+ return;
+ }
+ this.setState({ [tile.type]: this.state[tile.type] + 1});
+ this.setState({ total: this.state.total + 1 });
+ });
+ }
+
+ this.setState({
+ total: this.state.total,
+ wood: this.state.wood,
+ sheep: this.state.sheep,
+ stone: this.state.stone,
+ brick: this.state.brick,
+ wheat: this.state.wheat
+ });
+ }).catch((error) => {
+ console.error(error);
+ });
+ }
+
+ updateDimensions() {
+ const hasToolbar = false;
+
+ if (this.updateSizeTimer) {
+ clearTimeout(this.updateSizeTimer);
+ }
+
+ this.updateSizeTimer = setTimeout(() => {
+ const container = document.getElementById("root"),
+ offset = hasToolbar ? container.firstChild.offsetHeight : 0,
+ height = window.innerHeight - offset;
+
+ this.offsetY = offset;
+ this.width = window.innerWidth;
+ this.height = height;
+console.log("Update canvas size?");
+/*
+ if (this.canvas) {
+ this.canvas.width = this.width;
+ this.canvas.height = this.height;
+ this.canvas.style.top = `${offset}px`;
+ this.canvas.style.width = `${this.width}px`;
+ this.canvas.style.height = `${this.height}px`;
+ }
+*/
+ if (this.cards && this.cards.style) {
+ this.cards.style.top = `${offset}px`;
+ this.cards.style.width = `${this.width}px`;
+ this.cards.style.height = `${this.heigh}tpx`;
+ }
+
+ this.updateSizeTimer = 0;
+// this.drawFrame();
+ }, 250);
+ }
+
+ updateGame(game) {
+ console.log("Update Game", game);
+ this.game = game;
+ this.setState({ game: game });
+ }
+
+ updateMessage() {
+ const player = (this.game && this.game.color) ? this.game.players[this.game.color] : undefined,
+ name = this.game ? this.game.name : "";
+
+ let message = <>>;
+ if (this.state.pickName || !name) {
+ message = <>{message}Enter the name you would like to be known by, then press ENTER or select SET.>;
+ } else {
+ switch (this.game && this.game.state) {
+ case 'lobby':
+ message = <>{message}You are in the lobby as {name}.>;
+ if (!this.game.color) {
+ message = <>{message}You need to pick your color.>;
+ } else {
+ message = <>{message}You have selected .>;
+ }
+ message = <>{message}You can chat with other players below.>;
+ if (this.game.active < 2) {
+ message = <>{message}Once there are two or more players, you can select .>;
+ } else {
+ message = <>{message}There are enough players to start the game. Select when ready.>;
+ }
+ break;
+ case 'game-order':
+ if (!player) {
+ message = <>{message}This game as an observer as {name}.>;
+ message = <>{message}You can chat with other players below as {this.game.name}, but cannot play unless players go back to the Lobby.>;
+ } else {
+ if (!player.order) {
+ message = <>{message}You need to roll for game order. Click Roll Dice below.>;
+ } else {
+ message = <>{message}You rolled for game order. Waiting for all players to roll.>;
+ message = <>{message}
THIS IS THE END OF THE FUNCTIONALITY SO FAR>;
+ }
+ }
+ break;
+ case 'active':
+ if (!player) {
+ message = <>{message}This game is no longer in the lobby.
TODO: Override game state to allow Lobby mode while in-game>;
+ } else {
+ message = <>{message}
THIS IS THE END OF THE FUNCTIONALITY SO FAR>;
+ }
+ break;
+ case null:
+ case undefined:
+ case '':
+ message = <>{message}The game is in a wonky state. Sorry :(>;
+ break;
+ default:
+ message = <>{message}Game state is: {this.game.state}>;
+ break;
+ }
+ }
+ this.setState({ message: message });
+ }
+
+ componentDidMount() {
+ this.start = new Date();
+
+ const params = {};
+ if (this.id) {
+ console.log(`Loading game: ${this.id}`);
+ params.url = `${base}/api/v1/games/${this.id}`;
+ params.method = "GET"
+ } else {
+ console.log("Requesting new game.");
+ params.url = `${base}/api/v1/games/`;
+ params.method = "POST";
+ }
+
+ window.fetch(params.url, {
+ method: params.method,
+ cache: 'no-cache',
+ credentials: 'same-origin',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+// body: JSON.stringify(data) // body data type must match "Content-Type" header
+ }).then((res) => {
+ if (res.status < 400) {
+ return res;
+ }
+ let error;
+ if (!this.id) {
+ error = `Unable to create new game.`;
+ throw new Error(error);
+ }
+
+ error = `Unable to find game ${this.id}. Starting new game.`
+ console.log(error);
+ this.setState({ error: error });
+
+ params.url = `${base}/api/v1/games/${this.id}`;
+ params.method = "POST";
+
+ return window.fetch(params.url, {
+ method: params.method,
+ cache: 'no-cache',
+ credentials: 'same-origin',
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ });
+ }).then((res) => {
+ return res.json();
+ }).then((game) => {
+// console.log (`Game ${game.id} loaded ${moment().format()}.`);
+
+ if (!this.id) {
+ history.push(`${gamesPath}/${game.id}`);
+ }
+
+ this.updateGame(game);
+ this.updateMessage();
+
+ this.setState({ game: game, error: "" });
+ }).catch((error) => {
+ console.error(error);
+ this.setState({error: error.message});
+ }).then(() => {
+ this.resetGameLoad();
+ });
+
+ setTimeout(this.updateDimensions, 1000);
+ }
+
+ componentWillUnmount() {
+ if (this.loadTimer) {
+ clearTimeout(this.loadTimer);
+ }
+ if (this.updateSizeTimer) {
+ clearTimeout(this.updateSizeTimer);
+ this.updateSizeTimer = 0;
+ }
+ }
+
+ render() {
+ const game = this.state.game;
+
+ return (
+ this.el = el}>
+
+ { game &&
+
{ this.state.message }
+ {(this.state.pickName || !game.name) &&
}
+ {(!this.state.pickName && game.name) && <>
+
+
+
+ > }
+ }
+
+ this.cards = el}>
+ { game && game.state === "active" && <>
+
In hand
+
+
+
+
+
+
+
+
Available to play
+
+
Points
+
+
+
+
Stats
+
+
Points: 7
+
Cards: {this.state.total}
+
Roads remaining: 4
+
Longest road: 7
+
Cities remaining: 4
+
Settlements remaining: 5
+
+
+ > }
+
+
+ { this.state.error && {this.state.error}
}
+
+
+ );
+ }
+}
+export default withRouter(props => );