1
0
James Ketrenos a8995030ac Almost working with new react
Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
2022-01-03 21:13:47 -08:00

1146 lines
32 KiB
JavaScript
Executable File

import React, { useState, useEffect } from "react";
import "./Board.css";
import history from "./history.js";
import Paper from '@material-ui/core/Paper';
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 (
<Component
{...props}
router={{ location, navigate, params }}
/>
);
}
return ComponentWithRouterProp;
}
/* end of withRouter polyfill */
const assetsPath = '/assets';
const gamesPath = '/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(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 Tiles = (board) => {
const tiles = board.game.tiles;
[ "robber", "brick", "wood", "sheep", "stone", "wheat" ].forEach((type) => {
const image = new Image(),
file = "tiles-" + type + ".png";
tiles.forEach((tile) => {
if (tile.type === type) {
tile.image = image;
tile.x = 0;
tile.pos = { x: 0, y: 0 };
}
tile.jitter = 0;
});
image.addEventListener("load", (event) => {
console.log(`Done loading ${file}`);
window.requestAnimationFrame(board.drawFrame);
});
image.addEventListener("error", (event) => {
alert(`Error loading ${file}`);
});
image.src = `${assetsPath}/gfx/${file}`;
});
return tiles;
};
const Pips = (board) => {
const image = new Image(),
file = 'pip-numbers.png';
image.addEventListener("load", (event) => {
console.log(`Done loading ${file}`);
window.requestAnimationFrame(board.drawFrame);
});
image.addEventListener("error", (event) => {
alert(`Error loading ${file}`);
});
image.src = `${assetsPath}/gfx/${file}`;
return {
image: image,
pips: board.game.pips
};
};
const Border = (board, border) => {
const image = new Image(), file = border.file
border.image = image;
image.addEventListener("load", (event) => {
console.log(`Done loading ${file}`);
window.requestAnimationFrame(board.drawFrame);
});
image.addEventListener("error", (event) => {
alert(`Error loading ${file}`);
});
image.src = `${assetsPath}/gfx/${file}`;
return border;
};
const Table = (board) => {
const image = new Image(), file = "table.png";
image.addEventListener("load", (event) => {
console.log(`Done loading ${file}`);
window.requestAnimationFrame(board.drawFrame);
});
image.addEventListener("error", (event) => {
alert(`Error loading ${file}`);
});
image.src = `${assetsPath}/gfx/${file}`;
return image;
};
function shuffle(array) {
var currentIndex = array.length, temporaryValue, randomIndex;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
class Placard extends React.Component {
render() {
return (
<div className="Placard"
style={{backgroundImage:`url(${assetsPath}/gfx/placard-${this.props.type}.png)`}}>
</div>
);
}
};
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 (
<div className="Stack">
{ React.Children.map(array, i => (
<div className="Development"
style={{backgroundImage:`url(${assetsPath}/gfx/card-${this.props.type}${i}.png)`}}>
</div>
)) }
</div>
);
}
};
class Resource extends React.Component {
render() {
const array = new Array(Number(this.props.count ? this.props.count : 0));
return (
<>
{ array.length > 0 &&
<div className="Stack">
{ React.Children.map(array, i => (
<div className="Resource"
style={{backgroundImage:`url(${assetsPath}/gfx/card-${this.props.type}.png)`}}>
</div>
)) }
</div>
}
</>
);
}
};
const Chat = ({ game, promoteGameState }) => {
const chatInput = (event) => {
};
const chatKeyPress = (event) => {
if (event.key === "Enter") {
console.log(`Send: ${event.target.value}`);
promoteGameState({
chat: {
player: 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 = game.chat.map((item, index) => {
//const timestamp = moment(item.date - timeDelta).fromNow();
return (
<ListItem key={`msg-${index}`}>
<ListItemAvatar>
<Avatar className={classes[item.from]}>{item.from}</Avatar>
</ListItemAvatar>
<ListItemText primary={item.message} secondary={(<Moment fromNow interval={1000}/>)} />
</ListItem>
);
});
useEffect(() => {
document.querySelector(".chatInput").focus();
});
return (
<Paper className="Chat">
<List id="ChatList">
{ messages }
</List>
<TextField className="chatInput"
disabled={!game.activePlayer}
inputRef={input => input && input.focus()}
onChange={chatInput}
onKeyPress={chatKeyPress}
label={(<Moment format="h:mm:ss" interval={1000}/>)} variant="outlined"/>
</Paper>
);
}
/* This needs to take in a mechanism to declare the
* player's active item in the game */
const Players = ({ game, 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 != "", activePlayer == "", and name != "" */
if (selected && !game.activePlayer && name) {
change.players[selected] = { name: name }
promoteGameState(change)
return;
}
/* Leaving: selected = "", name = "", activePlayer != "" */
if (!selected && game.activePlayer) {
change.players[game.activePlayer] = { name: "" }
promoteGameState(change)
return;
}
/* Updating name: selected != "", activePlayer != "", name != "", name != activePlayer.name*/
if (selected &&
game.activePlayer &&
name &&
game.players[game.activePlayer].name !== name) {
change.players[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 game.players) {
const item = game.players[key];
players.push((
<ListItem key={`player-${key}`}>
<ListItemAvatar>
<Avatar className={classes[key]}>{key}</Avatar>
</ListItemAvatar>
<> { /* so flex-grow works we put in a fragment */ }
{ selected === key && item.name === "" &&
<TextField className="nameInput"
onChange={nameInput}
onKeyPress={nameKeyPress}
inputRef={input => input && input.focus()}
disabled={(name !== item.name) ? true: false}
label="Name"
variant="outlined" autoFocus/>
}
{ (selected !== key || item.name !== "") &&
<ListItemText primary={item.name} secondary={item.status} />
}
</>
<Switch edge="end"
disabled={(selected && selected !== key) ? true : false} checked={selected === key}
onChange={() => toggleSelected(key)}/>
</ListItem>
));
}
return (
<Paper className="Players">
<List>
{ players }
</List>
</Paper>
);
}
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
};
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.randomize = this.randomize.bind(this);
this.throwDice = this.throwDice.bind(this);
this.promoteGameState = this.promoteGameState.bind(this);
this.loadGame = this.loadGame.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
};
const params = {};
console.log(props.router);
if (props.router && props.router.params.id) {
console.log(`Loading game: ${props.router.params.id}`);
params.url = `api/v1/games/${props.router.params.id}`;
params.method = "GET"
} else {
console.log("Requesting new game.");
params.url = "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) {
const base = document.querySelector("base");
window.location.href = base ? base.href : "/";
console.log(`Unable to find game ${props.router.params.id}`);
throw new Error(`Unable to find requested game ${props.router.params.id}. Starting new one.`);
}
return res.json();
}).then((game) => {
console.log (`Game ${game.id} loaded.`);
if (!props.router.params.id) {
history.push(`${gamesPath}/${game.id}`);
}
this.game = game;
this.setState({ game: game });
this.pips = Pips(this);
this.tiles = Tiles(this);
this.table = Table(this);
this.borders = this.game.borders.map((file) => {
return Border(this, file);
});
this.loadTimer = window.setTimeout(this.loadGame, 1000);
}).catch((error) => {
console.error(error);
alert(error);
})
}
loadGame() {
if (this.loadTimer) {
window.clearTimeout(this.loadTimer);
this.loadTimer = null;
}
if (this.state.game) {
return;
}
window.fetch(`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 state loaded.`);
this.setState({ game: game });
}).catch((error) => {
console.error(error);
alert(error);
}).then(() => {
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;
}
window.fetch(`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.setState({ game: game });
}).catch((error) => {
console.error(error);
alert(error);
}).then(() => {
this.loadTimer = window.setTimeout(this.loadGame, 1000);
});
}
randomize() {
//this.borders = shuffle(this.borders);
this.tiles = shuffle(this.tiles);
this.tiles.forEach((tile) => {
tile.roads = [];
tile.settlements = [];
tile.jitter = Math.random() - 0.5;
});
this.closest.tile = null;
window.requestAnimationFrame(this.drawFrame);
}
keyUp(event) {
if (event.keyCode === 78) { /* n */
this.randomize();
return;
}
if (event.keyCode === 32) { /* space */
this.throwDice();
return;
}
}
throwDice() {
for (let i = 0; i < 2; i++) {
dice[i] = {
pips: Math.ceil(Math.random() * 6),
angle: Math.random() * Math.PI * 2,
jitter: (Math.random() - 0.5) * diceSize * 0.125
}
}
window.requestAnimationFrame(this.drawFrame);
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
});
}
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() {
if (this.updateSizeTimer) {
clearTimeout(this.updateSizeTimer);
}
this.updateSizeTimer = setTimeout(() => {
const container = document.getElementById("root"),
offset = container.firstChild.offsetHeight,
height = window.innerHeight - offset;
this.offsetY = offset;
this.width = window.innerWidth;
this.height = height;
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;
}
const ctx = this.canvas.getContext("2d");
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
ctx.save();
ctx.strokeStyle = 'white';
ctx.filleStyle = '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)";
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.);
});
}
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);
this.updateDimensions();
}
componentWillUnmount() {
if (this.updateSizeTimer) {
clearTimeout(this.updateSizeTimer);
this.updateSizeTimer = 0;
}
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 (
<div className="Board" ref={el => this.el = el}>
<canvas className="Display" ref={el => this.canvas = el}></canvas>
<div className="Cards" ref={el => this.cards = el}>
{ game &&
<div className="Game">
<Players game={game} promoteGameState={this.promoteGameState}/>
<Chat game={game} promoteGameState={this.promoteGameState}/>
</div>
}
<div>In hand</div>
<div className="Hand">
<Resource type="wood" count={this.state.wood}/>
<Resource type="wheat" count={this.state.wheat}/>
<Resource type="stone" count={this.state.stone}/>
<Resource type="brick" count={this.state.brick}/>
<Resource type="sheep" count={this.state.sheep}/>
</div>
<div>Available to play</div>
<div className="Hand">
<Development type="monopoly" count="1"/>
<Development type="army-" max="14" count="4"/>
<div className="Stack">
<Development type="vp-library" count="1"/>
<Development type="vp-market" count="1"/>
</div>
</div>
<div>Points</div>
<div className="Hand">
<div className="Stack">
<Development type="vp-library" count="1"/>
<Development type="vp-palace" count="1"/>
<Development type="army-" max="14" count="6"/>
</div>
</div>
<div className="Hand">
<Placard type="largest-army" count="1"/>
<Placard type="longest-road" count="1"/>
</div>
<div className="Statistics">
<div>Stats</div>
<div>
<div>Points: 7</div>
<div>Cards: {this.state.total} </div>
<div>Roads remaining: 4</div>
<div>Longest road: 7</div>
<div>Cities remaining: 4</div>
<div>Settlements remaining: 5</div>
</div>
</div>
</div>
</div>
);
}
}
export default withRouter(props => <Board {...props}/>);