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}> - this.canvas = 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 ( + {name} + ); +} + +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 => );