1146 lines
32 KiB
JavaScript
Executable File
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 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(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 = {};
|
|
|
|
if (props.router && props.router.params.id) {
|
|
console.log(`Loading game: ${props.router.params.id}`);
|
|
params.url = `${base}/api/v1/games/${props.router.params.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) {
|
|
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) => {
|
|
console.log(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(`${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 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(`${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.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);
|
|
}
|
|
console.log("Fix updateDimensions");
|
|
|
|
this.updateSizeTimer = setTimeout(() => {
|
|
const container = document.getElementById("root"),
|
|
offset = container.firstChild.offsetHeight,
|
|
height = window.innerHeight - offset;
|
|
return;
|
|
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);
|
|
|
|
setTimeout(this.updateDimensions, 1000);
|
|
}
|
|
|
|
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}/>);
|