import React, { 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 ListItemText from '@material-ui/core/ListItemText'; import ListItemAvatar from '@material-ui/core/ListItemAvatar'; import { makeStyles } from '@material-ui/core/styles'; import { deepOrange, lightBlue, red, grey } from '@material-ui/core/colors'; import Avatar from '@material-ui/core/Avatar'; import Switch from '@material-ui/core/Switch'; 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 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(deepOrange[500]), backgroundColor: deepOrange[500], }, W: { color: theme.palette.getContrastText(grey[100]), backgroundColor: grey[100], }, B: { color: theme.palette.getContrastText(lightBlue[500]), backgroundColor: lightBlue[500], }, })); const hexagonRatio = 1.1547005, tileHeight = 0.16, tileWidth = tileHeight * hexagonRatio, roadSize = tileHeight * 0.5 * (5. / 8.), settlementSize = tileHeight * 0.5 - roadSize, diceSize = 0.05, diceMargin = diceSize * 0.5 * Math.sqrt(2), dice = [ { pips: 0, jitter: 0, angle: 0 }, { pips: 0, jitter: 0, angle: 0 } ]; const loadImage = (board, file) => { if (file in images) { return images[file]; } const image = new Image(); images[file] = image; image.addEventListener("load", board.imageLoaded); image.addEventListener("error", board.imageLoadError); image.src = `${assetsPath}/gfx/${file}`; return image; } const Tiles = (board) => { const tiles = board.game.tiles; [ "robber", "brick", "wood", "sheep", "stone", "wheat" ].forEach((type) => { const file = "tiles-" + type + ".png", image = loadImage(board, file); tiles.forEach((tile) => { if (tile.type === type) { tile.image = image; tile.x = 0; tile.pos = { x: 0, y: 0 }; } tile.jitter = 0; }); }); return tiles; }; const Pips = (board) => { const file = 'pip-numbers.png', image = loadImage(board, file); return { image: image, pips: board.game.pips }; }; const Border = (board, border) => { const file = border.file, image = loadImage(board, file); border.image = image; return border; }; const Table = (board) => { const file = "table.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 chatInput = (event) => { }; const chatKeyPress = (event) => { if (event.key === "Enter") { console.log(`Send: ${event.target.value}`); promoteGameState({ chat: { player: board.game.activePlayer, message: event.target.value } }); event.target.value = ""; } }; const classes = useStyles(); useEffect(() => { const chatList = document.getElementById("ChatList"); chatList.scrollTop = chatList.scrollHeight - chatList.offsetHeight; }) //const timeDelta = game.timestamp - Date.now(); const messages = board.game.chat.map((item, index) => { return ( {item.from} )} /> ); }); useEffect(() => { document.querySelector(".chatInput").focus(); }); return ( { messages } input && input.focus()} onChange={chatInput} onKeyPress={chatKeyPress} label={()} variant="outlined"/> ); } const Action = ({ board }) => { const startClick = (event) => { board.setGameState("active").then((state) => { board.game.state = state; }); }; const newBoardClick = (event) => { return board.shuffleBoard(); }; const rollClick = (event) => { board.throwDice(); } return ( <> { board.game.state == 'lobby' && } ); } /* This needs to take in a mechanism to declare the * player's active item in the game */ const Players = ({ board, promoteGameState }) => { const [selected, setSelected] = useState(""); const [name, setName] = useState(""); const nameInput = (event) => { console.log(event.target.value); }; const nameKeyPress = (event) => { if (event.key === "Enter") { console.log(`Send: ${event.target.value}`); setName(event.target.value); } } useEffect(() => { const change = { players: {} }; /* Joining: selected != "" and name != "" */ if (selected && name && !board.game.activePlayer) { change.players[selected] = { name: name } promoteGameState(change) return; } /* Leaving: selected = "", name = "" */ if (!selected && board.game.activePlayer) { change.players[board.game.activePlayer] = { name: "" } promoteGameState(change) return; } /* Updating name: selected != "", name != "", name != board.name*/ if (selected && board.player && name && board.game.players[board.game.activePlayer].name !== name) { change.players[board.game.activePlayer] = { name: name } promoteGameState(change) return; } }); const toggleSelected = (key) => { if (selected === key) { setSelected(""); setName(""); } else { setSelected(key); } } const classes = useStyles(); const players = []; for (let key in board.game.players) { const item = board.game.players[key]; console.log(item); if (board.game.state != "lobby" && item.status == 'Not active') { continue; } players.push(( {key} <> { /* so flex-grow works we put in a fragment */ } { selected === key && item.name === "" && input && input.focus()} disabled={(name !== item.name) ? true: false} label="Name" variant="outlined" autoFocus/> } { (selected !== key || item.name !== "") && } toggleSelected(key)}/> )); } 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: "" }; 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.keyUp = this.keyUp.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.mouse = { x: 0, y: 0 }; this.radius = 0.317; this.loadTimer = null; this.game = null; this.pips = []; this.tiles = []; this.borders = []; this.table = 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) { console.log(`Done loading ${event.target.src}`); window.requestAnimationFrame(this.drawFrame); } imageLoadError(event) { this.setState({message: `Error loading ${event.target.src}`}); } 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.setState({ game: game, message: "Board shuffled!" }); }).catch((error) => { console.error(error); this.setState({message: 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) => { let message = game.status != "success" ? game.status : "Dice rolled!" if (game.status != "success") { game.dice = []; } this.updateGame(game); this.setState({ game: { ...this.state.game, dice: game.dice }, message: message } ); }).catch((error) => { console.error(error); this.setState({message: 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(`Unable to load state`); } return res.json(); }).then((game) => { console.log (`Game ${game.id} loaded ${moment().format()}.`); this.updateGame(game); this.setState({ game: game, message: "" }); }).catch((error) => { console.error(error); this.setState({message: 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) => { console.log (`Game state changed.`); this.updateGame(game); this.setState({ game: game, message: "" }); }).catch((error) => { console.error(error); this.setState({message: error.message}); }).then(() => { this.resetGameLoad(); }); } keyUp(event) { } 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.setState({ game: { ...this.state.game, state: game.state }, message: `Game state now ${game.state}.` }); }).catch((error) => { console.error(error); this.setState({message: 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 == 'lobby') { 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`; } 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.table) { 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); ctx.save(); ctx.strokeStyle = 'white'; ctx.fillStyle = 'rgba(0, 0, 0, 0)'; this.minSize = Math.min(this.canvas.height, this.canvas.width); /* * Table tiling: * Image width: 1080 * Left start: 32 * Right edge: 1010 (1010 - 32 = 978) * * If the view is wider than taller, then */ const tableLeft = 32 * this.table.width / 1080, tableRight = 1010 * this.table.width / 1080, tableLeaf = 978 * this.table.width / 1080; /* If view is taller than wide, tile the table vertically */ ctx.save(); if (this.canvas.height > this.canvas.width) { const tableHeight = this.canvas.width * this.table.height / this.table.width; for (let top = 0, step = 0; top < this.canvas.height; top += tableHeight, step++) { if (step % 2) { ctx.save(); ctx.translate(0, tableHeight - 1); ctx.transform(1, 0, 0, -1, 0, 0); ctx.drawImage(this.table, 0, 0, this.table.width, this.table.height, 0, 0, this.canvas.width, this.canvas.width * this.table.height / this.table.width); ctx.restore(); } else { ctx.drawImage(this.table, 0, 0, this.table.width, this.table.height, 0, 0, this.canvas.width, this.canvas.width * this.table.height / this.table.width); } ctx.translate(0, tableHeight); } } else { //const tableWidth = this.canvas.height * this.table.width / this.table.height; ctx.drawImage(this.table, 0, 0, tableRight, this.table.height, 0, 0, this.canvas.height * tableRight / this.table.height, this.canvas.height); let left = this.canvas.height * tableRight / this.table.height; while (left < this.canvas.width) { ctx.drawImage(this.table, tableLeft, 0, tableLeaf, this.table.height, left, 0, this.canvas.height * tableLeaf / this.table.height, this.canvas.height); left += this.canvas.height * tableLeaf / this.table.height; } } ctx.restore(); ctx.scale(this.minSize / hexagonRatio, this.minSize / hexagonRatio); ctx.translate(0.5 * hexagonRatio, 0.5 * hexagonRatio); ctx.lineWidth = 2. / this.minSize; /* Board dimensions: * ________ * /___1__| \ * / / \6\ * /2/ \ \ * / / \/\ * \/\ / / * \ \ /5/ * \3\______/_/ * \_|__4___/ * 0 0.3 0.6 1 */ ctx.save(); this.drawBorders(ctx); ctx.restore(); ctx.save(); this.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') { const roll = dice[0].pips + dice[1].pips; if (roll) this.tiles.forEach((tile) => { if (tile.pip.roll === roll) { ctx.save(); ctx.beginPath(); ctx.arc(tile.pos.x, tile.pos.y, tileHeight * 0.5, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.restore(); } }); } if (this.closest.tile) { ctx.save(); ctx.translate(this.closest.tile.pos.x, this.closest.tile.pos.y); ctx.strokeStyle = "red"; ctx.beginPath(); ctx.arc(0, 0, tileHeight * 0.5, 0, Math.PI * 2.); ctx.stroke(); /* road */ let angle = Math.round(this.closest.info.angle / (Math.PI / 3.)) * (Math.PI / 3.); ctx.strokeStyle = "rgb(64, 64, 255)"; ctx.fillStyle = "rgb(0, 0, 255)"; ctx.rotate(angle); ctx.translate(-tileHeight * 0.5, 0); ctx.beginPath(); ctx.rect(-roadSize * 0.125, -roadSize * 0.5, roadSize * 0.25, roadSize); ctx.fill(); ctx.stroke(); ctx.translate(tileHeight * 0.5, 0); ctx.rotate(-angle); /* village */ angle = (this.closest.info.angle - Math.PI / 6.); angle = Math.round(angle / (Math.PI / 3.)) * (Math.PI / 3.); angle += Math.PI / 6.; ctx.strokeStyle = "rgb(64, 64, 255)"; ctx.fillStyle = "rgb(0, 0, 255)"; ctx.rotate(angle); ctx.translate(-tileWidth * 0.5, 0); ctx.rotate(-angle); ctx.beginPath(); ctx.rect(-settlementSize * 0.5, -settlementSize * 0.5, settlementSize, settlementSize); ctx.fill(); ctx.stroke(); ctx.rotate(angle); ctx.translate(+tileWidth * 0.5, 0); ctx.rotate(-angle); ctx.restore(); } /* For 0.5 after mouse movement, there is an on * screen mouse helper. */ if (this.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, tileHeight * 0.5, 0, Math.PI * 2.); ctx.stroke(); ctx.fill(); } ctx.save(); ctx.translate(tileWidth * -2.5, -tileWidth * 2); ctx.rotate(Math.PI * -0.25) if (dice[0].pips) { ctx.translate(-0.5 * (diceSize + diceMargin), 0); this.drawDie(ctx, dice[0]); } if (dice[1].pips) { ctx.translate(diceSize + diceMargin, 0); this.drawDie(ctx, dice[1]); } ctx.restore(); ctx.restore(); } drawDie(ctx, die) { const radius = diceSize * 0.125, offset = diceSize * 0.5 - radius, pips = die.pips; ctx.save(); ctx.rotate(die.angle); ctx.translate(die.jitter, 0); ctx.strokeStyle = "#666"; ctx.fillStyle = "#444"; ctx.beginPath(); ctx.arc(-offset, -offset, radius, Math.PI, Math.PI * 1.5); ctx.arc(+offset, -offset, radius, Math.PI * 1.5, 0); ctx.arc(+offset, +offset, radius, 0, Math.PI * 0.5); ctx.arc(-offset, +offset, radius, Math.PI * 0.5, Math.PI); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.strokeStyle = "#bbb"; ctx.fillStyle = "#ddd"; /* center pip */ if (pips === 1 || pips === 3 || pips === 5) { ctx.beginPath(); ctx.arc(0, 0, diceSize * 0.0625, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } /* upper left pip */ if (pips === 2 || pips === 3 || pips === 4 || pips === 5 || pips === 6) { ctx.beginPath(); ctx.arc(-diceSize * 0.250, -diceSize * 0.250, diceSize * 0.0625, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } /* upper right pip */ if (pips === 4 || pips === 5 || pips === 6) { ctx.beginPath(); ctx.arc(+diceSize * 0.250, -diceSize * 0.250, diceSize * 0.0625, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } /* lower right pip */ if (pips === 2 || pips === 3 || pips === 4 || pips === 5 || pips === 6) { ctx.beginPath(); ctx.arc(+diceSize * 0.250, +diceSize * 0.250, diceSize * 0.0625, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } /* lower left pip */ if (pips === 4 || pips === 5 || pips === 6) { ctx.beginPath(); ctx.arc(-diceSize * 0.250, +diceSize * 0.250, diceSize * 0.0625, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } /* middle left and right pip */ if (pips === 6) { ctx.beginPath(); ctx.arc(-diceSize * 0.250, 0, diceSize * 0.0625, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.arc(+diceSize * 0.250, 0, diceSize * 0.0625, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } ctx.restore(); } drawPips(ctx) { const image = this.pips.image, pipSize = 0.06; function drawTile(tile, angle, radius) { tile.pos.x = Math.sin(-angle) * radius; tile.pos.y = Math.cos(-angle) * radius; const image = tile.image; ctx.save(); ctx.rotate(angle); ctx.translate(0., radius); ctx.rotate(-angle + Math.PI * 1. / 6.); ctx.drawImage(image, tile.x * image.width, tile.y * image.height, image.width, image.height / 4., -tileWidth * 0.5, -tileHeight * 0.5, tileWidth, tileHeight); ctx.restore(); } function drawPip(pip, angle, radius, jitter) { ctx.save(); ctx.rotate(angle); ctx.translate(0., radius); /* Offset into a random direction by a random amount */ ctx.rotate(Math.PI * 2. * jitter); ctx.translate(0, tileHeight * 0.1 * jitter); /* Undo random rotation for position, and add random rotation * for pip placement */ ctx.rotate(-angle - Math.PI * 2. * jitter + jitter * 0.4); ctx.drawImage(image, pip.x * image.width, pip.y * image.height, image.width / 6., image.height / 6., -pipSize * 0.5, -pipSize * 0.5, pipSize, pipSize); ctx.restore(); } let angle, radius = this.radius, index = 1, pip; //, roll = dice[0].pips + dice[1].pips; /* Outer row */ angle = 0; for (let i = 0; i < 12; i++) { angle -= Math.PI * 2. / 12.; if (this.tiles[i].type === "robber") { pip = this.pips.pips[0] } else { 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); } /* Middle row */ angle = Math.PI * 2. / 12.; radius = this.radius * 0.5; for (let i = 12; i < 18; i++) { angle -= Math.PI * 2. / 6.; if (this.tiles[i].type === "robber") { pip = this.pips.pips[0] } else { pip = this.pips.pips[index++]; } this.tiles[i].pip = pip; drawTile(this.tiles[i], angle, radius); drawPip(pip, angle, radius, this.tiles[i].jitter); } /* Center */ let i = 18; if (this.tiles[i].type === "robber") { pip = this.pips.pips[0] } else { pip = this.pips.pips[index++]; } this.tiles[i].pip = pip; drawTile(this.tiles[i], 0, 0); drawPip(pip, 0, 0, this.tiles[i].jitter); } drawBorders(ctx) { ctx.rotate(Math.PI); const offset = 0.18; this.borders.forEach((border, index) => { ctx.translate(0, this.radius); const image = border.image; ctx.drawImage(image, -offset, 0, 0.5, 0.5 * image.height / image.width); ctx.translate(0, -this.radius); ctx.rotate(Math.PI * 2. / 6.); }); } updateGame(game) { this.game = game; if (game.state === "invalid") { return; } this.pips = Pips(this); this.tiles = Tiles(this); this.table = Table(this); this.borders = this.game.borders.map((file) => { return Border(this, file); }); window.requestAnimationFrame(this.drawFrame); } componentDidMount() { this.start = new Date(); document.addEventListener("keyup", this.keyUp); 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 message; if (!this.id) { message = `Unable to create new game.`; throw new Error(message); } message = `Unable to find game ${this.id}. Starting new game.` console.log(message); this.setState({ message: message }); 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.setState({ game: game, message: "" }); }).catch((error) => { console.error(error); this.setState({message: 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; } for (let image in images) { image.removeEventListener("load", this.imageLoaded); image.removeEventListener("error", this.imageLoadError); delete images[image]; } document.removeEventListener("keyup", this.keyUp); 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}>
this.cards = el}> { game &&
{ this.state.message != "" &&
{this.state.message}
}
} { 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
}
); } } export default withRouter(props => );