543 lines
15 KiB
JavaScript
543 lines
15 KiB
JavaScript
import React, { useState } from "react";
|
|
import "./Board.css";
|
|
|
|
const base = process.env.PUBLIC_URL;
|
|
const assetsPath = `${base}/assets`;
|
|
|
|
const Board = ({ table, game }) => {
|
|
const rows = [3, 4, 5, 4, 3, 2]; /* The final row of 2 is to place roads and corners */
|
|
const [signature, setSignature] = useState("");
|
|
const [pips, setPips] = useState(<></>);
|
|
const [borders, setBorders] = useState(<></>);
|
|
const [tiles, setTiles] = useState(<></>);
|
|
const [corners, setCorners] = useState(<></>);
|
|
const [roads, setRoads] = useState(<></>);
|
|
|
|
const [transform, setTransform] = useState(1);
|
|
|
|
const
|
|
hexRatio = 1.1547,
|
|
tileWidth = 67,
|
|
tileHalfWidth = tileWidth * 0.5,
|
|
tileHeight = tileWidth * hexRatio,
|
|
tileHalfHeight = tileHeight * 0.5,
|
|
radius = tileHeight * 2,
|
|
borderOffsetX = 86, /* ~1/10th border image width... hand tuned */
|
|
borderOffsetY = 3;
|
|
|
|
/* Actual sizing */
|
|
const
|
|
tileImageWidth = 90, /* Based on hand tuned and image width */
|
|
tileImageHeight = tileImageWidth/hexRatio,
|
|
borderImageWidth = (2 + 2/3) * tileImageWidth, /* 2.667 * .Tile.width */
|
|
borderImageHeight = borderImageWidth * 0.29; /* 0.29 * .Border.height */
|
|
|
|
const Tile = ({tile}) => {
|
|
const onClick = (event) => {
|
|
console.log(`Tile clicked: ${tile.index}`);
|
|
};
|
|
|
|
return <div className="Tile"
|
|
onClick={onClick}
|
|
data-index={tile.index}
|
|
style={{
|
|
top: `${tile.top}px`,
|
|
left: `${tile.left}px`,
|
|
width: `${tileImageWidth}px`,
|
|
height: `${tileImageHeight}px`,
|
|
backgroundImage: `url(${assetsPath}/gfx/tiles-${tile.type}.png)`,
|
|
backgroundPositionY: `-${tile.card*tileHeight}px`
|
|
}}
|
|
><div className="Tile-Shape"/></div>;
|
|
};
|
|
|
|
const Road = ({road}) => {
|
|
const onClick = (event) => {
|
|
console.log(`Road clicked: ${road.index}`);
|
|
table.placeRoad(road.index);
|
|
};
|
|
|
|
return <div className="Road"
|
|
onClick={onClick}
|
|
data-index={road.index}
|
|
style={{
|
|
transform: `translate(-50%, -50%) rotate(${road.angle}deg)`,
|
|
top: `${road.top}px`,
|
|
left: `${road.left}px`
|
|
}}
|
|
><div className="Road-Shape"/></div>;
|
|
};
|
|
|
|
const Corner = ({corner}) => {
|
|
const onClick = (event) => {
|
|
// console.log(`Corner ${corner.index}:`, game.layout.corners[corner.index]);
|
|
if (event.currentTarget.getAttribute('data-type') === 'settlement') {
|
|
table.placeCity(corner.index);
|
|
} else {
|
|
table.placeSettlement(corner.index);
|
|
}
|
|
return;
|
|
};
|
|
|
|
return <div className="Corner"
|
|
onClick={onClick}
|
|
data-index={corner.index}
|
|
style={{
|
|
top: `${corner.top}px`,
|
|
left: `${corner.left}px`
|
|
}}
|
|
><div className="Corner-Shape"/></div>;
|
|
};
|
|
|
|
const Pip = ({pip}) => {
|
|
const onClick = (event) => {
|
|
// console.log(`Pip ${pip.index}:`, game.layout.corners[pip.index]);
|
|
table.placeRobber(pip.index);
|
|
return;
|
|
};
|
|
|
|
return <div className="Pip"
|
|
onClick={onClick}
|
|
data-roll={pip.roll}
|
|
data-index={pip.index}
|
|
style={{
|
|
top: `${pip.top}px`,
|
|
left: `${pip.left}px`,
|
|
backgroundImage: `url(${assetsPath}/gfx/pip-numbers.png)`,
|
|
backgroundPositionX: `${ 100. * (pip.order % 6) / 5.}%`,
|
|
backgroundPositionY: `${ 100 * Math.floor(pip.order / 6) / 5. }%`
|
|
}}
|
|
><div className="Pip-Shape"/></div>;
|
|
};
|
|
|
|
const generateRoads = () => {
|
|
let row = 0, rowCount = 0;
|
|
let y = -2.5 + tileHalfWidth - (rows.length - 1) * 0.5 * tileWidth,
|
|
x = -tileHalfHeight -(rows[row] - 1) * 0.5 * tileHeight;
|
|
|
|
let index = 0;
|
|
let road;
|
|
|
|
const corners = [];
|
|
|
|
for (let i = 0; i < 21; i++) {
|
|
const lastRow = row === rows.length - 1;
|
|
if (row > 2 && rowCount === 0) {
|
|
road = {
|
|
index: index++,
|
|
angle: -60,
|
|
top: y-0.5*tileHalfHeight,
|
|
left: x-tileHalfHeight
|
|
};
|
|
corners.push(<Road key={`road-${index}}`} road={road}/>);
|
|
}
|
|
|
|
road = {
|
|
index: index++,
|
|
angle: 240,
|
|
top: y,
|
|
left: x
|
|
};
|
|
corners.push(<Road key={`road-${index}}`} road={road}/>);
|
|
|
|
road = {
|
|
index: index++,
|
|
angle: -60,
|
|
top: y-0.5*tileHalfHeight,
|
|
left: x+tileHalfHeight
|
|
};
|
|
corners.push(<Road key={`road-${index}}`} road={road}/>);
|
|
|
|
if (!lastRow) {
|
|
road = {
|
|
index: index++,
|
|
angle: 0,
|
|
top: y,
|
|
left: x
|
|
};
|
|
corners.push(<Road key={`road-${index}}`} road={road}/>);
|
|
}
|
|
|
|
if (++rowCount === rows[row]) {
|
|
if (!lastRow) {
|
|
road = {
|
|
index: index++,
|
|
angle: 0,
|
|
top: y,
|
|
left: x+2.*tileHalfHeight
|
|
};
|
|
corners.push(<Road key={`road-${index}}`} road={road}/>);
|
|
}
|
|
|
|
if (row > 2) {
|
|
road = {
|
|
index: index++,
|
|
angle: 60,
|
|
top: y-0.5*tileHalfHeight,
|
|
left: x+3.*tileHalfHeight
|
|
};
|
|
corners.push(<Road key={`road-${index}}`} road={road}/>);
|
|
}
|
|
|
|
row++;
|
|
rowCount = 0;
|
|
y += tileHeight - 10.5;
|
|
x = -tileHalfHeight - (rows[row] - 1) * 0.5 * tileHeight;
|
|
} else {
|
|
x += tileHeight;
|
|
}
|
|
}
|
|
return corners;
|
|
}
|
|
|
|
const generateCorners = () => {
|
|
let row = 0, rowCount = 0;
|
|
let y = -8 + 0.5 * tileHalfWidth - (rows.length - 1) * 0.5 * tileWidth,
|
|
x = -tileHalfHeight -(rows[row] - 1) * 0.5 * tileHeight;
|
|
let index = 0;
|
|
const corners = [];
|
|
let corner;
|
|
|
|
for (let i = 0; i < 21; i++) {
|
|
if (row > 2 && rowCount === 0) {
|
|
corner = {
|
|
index: index++,
|
|
top: y-0.5*tileHalfHeight,
|
|
left: x-tileHalfHeight
|
|
};
|
|
corners.push(<Corner key={`corner-${index}}`} corner={corner}/>);
|
|
}
|
|
|
|
corner = {
|
|
index: index++,
|
|
top: y,
|
|
left: x
|
|
};
|
|
corners.push(<Corner key={`corner-${index}}`} corner={corner}/>);
|
|
|
|
corner = {
|
|
index: index++,
|
|
top: y-0.5*tileHalfHeight,
|
|
left: x+tileHalfHeight
|
|
};
|
|
corners.push(<Corner key={`corner-${index}}`} corner={corner}/>);
|
|
|
|
if (++rowCount === rows[row]) {
|
|
corner = {
|
|
index: index++,
|
|
top: y,
|
|
left: x+2.*tileHalfHeight
|
|
};
|
|
corners.push(<Corner key={`corner-${index}}`} corner={corner}/>);
|
|
|
|
if (row > 2) {
|
|
corner = {
|
|
index: index++,
|
|
top: y-0.5*tileHalfHeight,
|
|
left: x+3.*tileHalfHeight
|
|
};
|
|
corners.push(<Corner key={`corner-${index}}`} corner={corner}/>);
|
|
}
|
|
|
|
row++;
|
|
rowCount = 0;
|
|
y += tileHeight - 10.5;
|
|
x = -tileHalfHeight - (rows[row] - 1) * 0.5 * tileHeight;
|
|
} else {
|
|
x += tileHeight;
|
|
}
|
|
}
|
|
return corners;
|
|
}
|
|
|
|
const generatePips = () => {
|
|
let row = 0, rowCount = 0;
|
|
let y = tileHalfWidth - (rows.length - 1) * 0.5 * tileWidth,
|
|
x = -(rows[row] - 1) * 0.5 * tileHeight;
|
|
let index = 0;
|
|
let pip;
|
|
return game.pipOrder.map(order => {
|
|
pip = {
|
|
roll: game.pips[order].roll,
|
|
robber: index === game.robber,
|
|
index: index++,
|
|
top: y,
|
|
left: x,
|
|
order: order
|
|
};
|
|
const div = <Pip
|
|
pip={pip}
|
|
key={`pip-${order}`}
|
|
/>;
|
|
|
|
if (++rowCount === rows[row]) {
|
|
row++;
|
|
rowCount = 0;
|
|
y += tileWidth;
|
|
x = - (rows[row] - 1) * 0.5 * tileHeight;
|
|
} else {
|
|
x += tileHeight;
|
|
}
|
|
|
|
return div;
|
|
});
|
|
};
|
|
|
|
const generateTiles = () => {
|
|
let row = 0, rowCount = 0;
|
|
let y = tileHalfWidth - (rows.length - 1) * 0.5 * tileWidth,
|
|
x = -(rows[row] - 1) * 0.5 * tileHeight;
|
|
let index = 0;
|
|
return game.tileOrder.map(order => {
|
|
const tile = Object.assign({},
|
|
game.tiles[order],
|
|
{ index: index++, left: x, top: y});
|
|
|
|
let div = <Tile
|
|
key={`${tile.type}-${tile.card}`}
|
|
tile={tile}
|
|
/>;
|
|
|
|
if (++rowCount === rows[row]) {
|
|
row++;
|
|
rowCount = 0;
|
|
y += tileWidth;
|
|
x = - (rows[row] - 1) * 0.5 * tileHeight;
|
|
} else {
|
|
x += tileHeight;
|
|
}
|
|
|
|
return div;
|
|
});
|
|
};
|
|
|
|
const generateBorders = () => {
|
|
const sides = 6;
|
|
let side = -1;
|
|
return game.borderOrder.map(order => {
|
|
const border = game.borders[order];
|
|
side++;
|
|
let x = + Math.sin(Math.PI - side / sides * 2. * Math.PI) * radius,
|
|
y = Math.cos(Math.PI - side / sides * 2. * Math.PI) * radius;
|
|
let prev = (order === 0) ? 6 : order;
|
|
const file = `borders-${order+1}.${prev}.png`;
|
|
return <div
|
|
key={`border-${order}`}
|
|
className="Border"
|
|
border={border}
|
|
style={{
|
|
width: `${borderImageWidth}px`,
|
|
height: `${borderImageHeight}px`,
|
|
top: `${y}px`,
|
|
left: `${x}px`,
|
|
transform: `rotate(${side*(360/sides)}deg) translate(${borderOffsetX}px, ${borderOffsetY}px) scale(-1, -1)`,
|
|
backgroundImage: `url(${assetsPath}/gfx/${file} )`
|
|
}}
|
|
/>;
|
|
});
|
|
};
|
|
|
|
/* If the game is loaded, and the signature is different,
|
|
* regenerate everything */
|
|
if (game && game.signature !== signature) {
|
|
console.log(`Generate for ${game.signature}`);
|
|
setPips(generatePips);
|
|
setBorders(generateBorders);
|
|
setTiles(generateTiles);
|
|
setCorners(generateCorners);
|
|
setRoads(generateRoads);
|
|
setSignature(game.signature);
|
|
} else {
|
|
if (!game) {
|
|
console.log(`No game in Board`);
|
|
}
|
|
}
|
|
|
|
/* Adjust the 'transform: scale' for the BoardBox
|
|
* so the board fills the Board
|
|
*
|
|
* The board is H tall, and H * hexRatio wide */
|
|
const board = document.querySelector('.Board');
|
|
if (board) {
|
|
const width = board.offsetWidth,
|
|
height = board.offsetHeight;
|
|
let _transform;
|
|
if (height * hexRatio > width) {
|
|
_transform = width / (450. * hexRatio);
|
|
} else {
|
|
_transform = height / (450.);
|
|
}
|
|
if (_transform !== transform) {
|
|
const boardBox = board.querySelector('.BoardBox');
|
|
if (boardBox) {
|
|
console.log(`Setting transofrm scale to ${_transform}`);
|
|
boardBox.style.transform = `scale(${_transform})`;
|
|
}
|
|
setTransform(_transform);
|
|
}
|
|
}
|
|
|
|
if (game && game.turn) {
|
|
let nodes = document.querySelectorAll('.Active');
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
nodes[i].classList.remove('Active');
|
|
}
|
|
if (game.turn.roll) {
|
|
nodes = document.querySelectorAll(`.Pip[data-roll="${game.turn.roll}"]`);
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
const index = nodes[i].getAttribute('data-index');
|
|
if (index !== null) {
|
|
const tile = document.querySelector(`.Tile[data-index="${index}"]`);
|
|
if (tile) {
|
|
tile.classList.add('Active');
|
|
}
|
|
}
|
|
nodes[i].classList.add('Active');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (game && game.placements) {
|
|
/* Set color and type based on placement data from the server */
|
|
game.placements.corners.forEach((corner, index) => {
|
|
const el = document.querySelector(`.Corner[data-index="${index}"]`);
|
|
if (!el) {
|
|
return;
|
|
}
|
|
if (!corner.color) {
|
|
el.removeAttribute('data-color');
|
|
el.removeAttribute('data-type');
|
|
} else {
|
|
el.setAttribute('data-color', corner.color);
|
|
el.setAttribute('data-type', corner.type);
|
|
}
|
|
});
|
|
game.placements.roads.forEach((road, index) => {
|
|
const el = document.querySelector(`.Road[data-index="${index}"]`);
|
|
if (!el) {
|
|
return;
|
|
}
|
|
if (!road.color) {
|
|
el.removeAttribute('data-color');
|
|
} else {
|
|
if (road.longestRoad) {
|
|
if (road.longestRoad === game.longestRoadLength) {
|
|
el.classList.add('LongestRoad');
|
|
} else {
|
|
el.classList.remove('LongestRoad');
|
|
}
|
|
el.setAttribute('data-longest', road.longestRoad);
|
|
} else {
|
|
el.removeAttribute('data-longest');
|
|
}
|
|
el.setAttribute('data-color', road.color);
|
|
}
|
|
});
|
|
/* Clear all 'Option' targets */
|
|
let nodes = document.querySelectorAll(`.Option`);
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
nodes[i].classList.remove('Option');
|
|
}
|
|
|
|
/* Add 'Option' based on game.turn.limits */
|
|
if (game.turn && game.turn.limits) {
|
|
if (game.turn.limits['roads']) {
|
|
game.turn.limits['roads'].forEach(index => {
|
|
const el = document.querySelector(`.Road[data-index="${index}"]`);
|
|
if (!el) {
|
|
return;
|
|
}
|
|
el.classList.add('Option');
|
|
});
|
|
}
|
|
if (game.turn.limits['corners']) {
|
|
game.turn.limits['corners'].forEach(index => {
|
|
const el = document.querySelector(`.Corner[data-index="${index}"]`);
|
|
if (!el) {
|
|
return;
|
|
}
|
|
el.classList.add('Option');
|
|
});
|
|
}
|
|
if (game.turn.limits['tiles']) {
|
|
game.turn.limits['tiles'].forEach(index => {
|
|
const el = document.querySelector(`.Tile[data-index="${index}"]`);
|
|
if (!el) {
|
|
return;
|
|
}
|
|
el.classList.add('Option');
|
|
});
|
|
}
|
|
if (game.turn.limits['pips']) {
|
|
game.turn.limits['pips'].forEach(index => {
|
|
const el = document.querySelector(`.Pip[data-index="${index}"]`);
|
|
if (!el) {
|
|
return;
|
|
}
|
|
el.classList.add('Option');
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (game) {
|
|
let nodes = document.querySelectorAll(`.Pip.Robber`);
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
nodes[i].classList.remove('Robber');
|
|
[ 'Robert', 'Roberta', 'Velocirobber' ].forEach(robberName =>
|
|
nodes[i].classList.remove(robberName)
|
|
);
|
|
}
|
|
|
|
if (game.robber !== undefined) {
|
|
const el = document.querySelector(`.Pip[data-index="${game.robber}"]`);
|
|
if (el) {
|
|
el.classList.add('Robber');
|
|
el.classList.add(game.robberName);
|
|
}
|
|
}
|
|
}
|
|
|
|
const canAction = (action) => {
|
|
return (game && game.turn && Array.isArray(game.turn.actions) && game.turn.actions.indexOf(action) !== -1);
|
|
};
|
|
|
|
const canRoad = (canAction('place-road')
|
|
&& game.turn.color === game.color
|
|
&& (game.state === 'initial-placement' || game.state === 'normal'));
|
|
|
|
const canCorner = ((canAction('place-settlement') || canAction('place-city'))
|
|
&& game.turn.color === game.color
|
|
&& (game.state === 'initial-placement' || game.state === 'normal'));
|
|
|
|
const canPip = (canAction('place-robber')
|
|
&& game.turn.color === game.color
|
|
&& (game.state === 'initial-placement' || game.state === 'normal'));
|
|
|
|
return (
|
|
<div className="Board">
|
|
<div className="BoardBox">
|
|
<div className="Borders" disabled>
|
|
{ borders }
|
|
</div>
|
|
{ game && <>
|
|
<div className="Tiles" disabled>
|
|
{ tiles }
|
|
</div>
|
|
<div className="Pips" disabled={!canPip}>
|
|
{ pips }
|
|
</div>
|
|
<div className="Corners" disabled={!canCorner}>
|
|
{ corners }
|
|
</div>
|
|
<div className="Roads" disabled={!canRoad}>
|
|
{ roads }
|
|
</div>
|
|
</> }
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Board;
|