1
0
James Ketrenos 588777af90 Add sheeps
Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
2022-06-23 14:07:53 -07:00

857 lines
26 KiB
JavaScript

import React, { useEffect, useState, useContext, useRef, useMemo } from "react";
import equal from "fast-deep-equal";
import { assetsPath } from "./Common.js";
import "./Board.css";
import { GlobalContext } from "./GlobalContext.js";
import { Flock } from "./Bird.js";
import { Herd } from "./Sheep.js";
const rows = [3, 4, 5, 4, 3, 2]; /* The final row of 2 is to place roads and corners */
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 Board = () => {
const { ws } = useContext(GlobalContext);
const board = useRef();
const [transform, setTransform] = useState(1.);
const [pipElements, setPipElements] = useState(<></>);
const [borderElements, setBorderElements] = useState(<></>);
const [tileElements, setTileElements] = useState(<></>);
const [cornerElements, setCornerElements] = useState(<></>);
const [roadElements, setRoadElements] = useState(<></>);
const [ signature, setSignature ] = useState("");
const [ generated, setGenerated ] = useState("");
const [ robber, setRobber ] = useState(-1);
const [ robberName, setRobberName ] = useState([]);
const [ pips, setPips ] = useState();
const [ pipOrder, setPipOrder ] = useState();
const [ borders, setBorders ] = useState();
const [ borderOrder, setBorderOrder ] = useState();
const [animationSeeds, setAnimationSeeds] = useState();
const [ tiles, setTiles ] = useState();
const [ tileOrder, setTileOrder ] = useState([]);
const [ placements, setPlacements ] = useState(undefined);
const [ turn, setTurn ] = useState({});
const [ state, setState ] = useState("");
const [ color, setColor ] = useState("");
const [ rules, setRules ] = useState({});
const [ longestRoadLength, setLongestRoadLength ] = useState(0);
const fields = useMemo(() => [
'signature', 'robber', 'robberName',
'pips', 'pipOrder', 'borders', 'borderOrder', 'tiles', 'tileOrder',
'placements', 'turn', 'state', 'color', 'longestRoadLength',
'rules', 'animationSeeds'
], []);
console.log(`board - ws`, ws);
const onWsMessage = (event) => {
if (ws && ws !== event.target) {
console.error(`Disconnect occur?`);
}
const data = JSON.parse(event.data);
switch (data.type) {
case 'game-update':
console.log(`board - game update`, data.update);
if ('robber' in data.update && data.update.robber !== robber) {
setRobber(data.update.robber);
}
if ('robberName' in data.update
&& data.update.robberName !== robberName) {
setRobberName(data.update.robberName);
}
if ('state' in data.update && data.update.state !== state) {
setState(data.update.state);
}
if ('rules' in data.update
&& !equal(data.update.rules, rules)) {
setRules(data.update.rules);
}
if ('color' in data.update && data.update.color !== color) {
setColor(data.update.color);
}
if ('longestRoadLength' in data.update
&& data.update.longestRoadLength !== longestRoadLength) {
setLongestRoadLength(data.update.longestRoadLength);
}
if ('turn' in data.update) {
if (!equal(data.update.turn, turn)) {
console.log(`board - turn`, data.update.turn);
setTurn(data.update.turn);
}
}
if ('placements' in data.update && !equal(data.update.placements, placements)) {
console.log(`board - placements`, data.update.placements);
setPlacements(data.update.placements);
}
/* The following are only updated if there is a new game
* signature */
if ('pipOrder' in data.update && !equal(data.update.pipOrder, pipOrder)) {
console.log(`board - setting new pipOrder`);
setPipOrder(data.update.pipOrder);
}
if ('borderOrder' in data.update && !equal(data.update.borderOrder, borderOrder)) {
console.log(`board - setting new borderOrder`);
setBorderOrder(data.update.borderOrder);
}
if ('animationSeeds' in data.update && !equal(data.update.animationSeeds, animationSeeds)) {
console.log(`board - setting new animationSeeds`);
setAnimationSeeds(data.update.animationSeeds);
}
if ('tileOrder' in data.update && !equal(data.update.tileOrder, tileOrder)) {
console.log(`board - setting new tileOrder`);
setTileOrder(data.update.tileOrder);
}
if (data.update.signature !== signature) {
console.log(`board - setting new signature`);
setSignature(data.update.signature);
}
/* This is permanent static data from the server -- do not update
* once set */
if ('pips' in data.update && !pips) {
console.log(`board - setting new static pips`);
setPips(data.update.pips);
}
if ('tiles' in data.update && !tiles) {
console.log(`board - setting new static tiles`);
setTiles(data.update.tiles);
}
if ('borders' in data.update && !borders) {
console.log(`board - setting new static borders`);
setBorders(data.update.borders);
}
break;
default:
break;
}
};
const refWsMessage = useRef(onWsMessage);
useEffect(() => { refWsMessage.current = onWsMessage; });
useEffect(() => {
if (!ws) { return; }
console.log('board - bind');
const cbMessage = e => refWsMessage.current(e);
ws.addEventListener('message', cbMessage);
return () => {
console.log('board - unbind');
ws.removeEventListener('message', cbMessage);
}
}, [ws]);
useEffect(() => {
if (!ws) { return; }
ws.send(JSON.stringify({
type: 'get',
fields
}));
}, [ws, fields]);
useEffect(() => {
const boardBox = board.current.querySelector('.BoardBox');
if (boardBox) {
console.log(`board - setting transform scale to ${transform}`);
boardBox.style.transform = `scale(${transform})`;
}
}, [transform]);
const onResize = () => {
if (!board.current) {
return;
}
/* Adjust the 'transform: scale' for the BoardBox
* so the board fills the Board
*
* The board is H tall, and H * hexRatio wide */
const width = board.current.offsetWidth,
height = board.current.offsetHeight;
let _transform;
if (height * hexRatio > width) {
_transform = width / (450. * hexRatio);
} else {
_transform = height / (450.);
}
if (transform !== _transform) {
setTransform(_transform);
}
};
const refOnResize = useRef(onResize);
useEffect(() => { refOnResize.current = onResize; });
useEffect(() => {
const cbOnResize = e => refOnResize.current(e);
window.addEventListener('resize', cbOnResize);
return () => {
window.removeEventListener('resize', cbOnResize);
}
}, [refOnResize]);
onResize();
useEffect(() => {
if (!ws) {
return;
}
console.log(`Generating static corner data... should only occur once per reload or socket reconnect.`);
const onCornerClicked = (event, corner) => {
let type;
if (event.currentTarget.getAttribute('data-type') === 'settlement') {
type = 'place-city';
} else {
type = 'place-settlement';
}
ws.send(JSON.stringify({
type, index: corner.index
}));
};
const Corner = ({corner}) => {
return <div className="Corner"
onClick={(event) => { onCornerClicked(event, corner) }}
data-index={corner.index}
style={{
top: `${corner.top}px`,
left: `${corner.left}px`
}}
><div className="Corner-Shape"/></div>;
};
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;
};
setCornerElements(generateCorners());
}, [ws, setCornerElements]);
useEffect(() => {
if (!ws) {
return;
}
console.log(`Generating static road data... should only occur once per reload or socket reconnect.`);
const Road = ({road}) => {
const onRoadClicked = (road) => {
console.log(`Road clicked: ${road.index}`);
if (!ws) { console.error(`board - onRoadClicked - ws is NULL`); return; }
ws.send(JSON.stringify({
type: 'place-road', index: road.index
}));
};
return <div className="Road"
onClick={() => { onRoadClicked(road) }}
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 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;
}
setRoadElements(generateRoads());
}, [ws, setRoadElements]);
/* Generate Pip, Tile, and Border elements */
useEffect(() => {
if (!ws) {
return;
}
console.log(`board - Generate pip, border, and tile elements`);
const Pip = ({pip, className}) => {
const onPipClicked = (pip) => {
if (!ws) { console.error(`board - sendPlacement - ws is NULL`); return; }
ws.send(JSON.stringify({
type: 'place-robber', index: pip.index
}));
};
return <div className={`Pip ${className}`}
onClick={() => { onPipClicked(pip) }}
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 generatePips = function (pipOrder) {
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 pipOrder.map(order => {
let volcano = false;
pip = {
roll: pips[order].roll,
index: index++,
top: y,
left: x,
order: order
};
if ('volcano' in rules
&& rules[`volcano`].enabled
&& pip.roll === 7) {
pip.order = pips.findIndex(
pip => pip.roll === rules[`volcano`].number);
pip.roll = rules[`volcano`].number;
volcano = true;
}
const div = <Pip className={volcano ? "Volcano" : ""}
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 Tile = ({tile}) => {
const 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`
};
if (tile.type === 'volcano') {
style.transform = `rotate(-90deg)`;
style.top = `${tile.top + 6}px`;
style.transformOrigin = '0% 50%';
}
return <div className="Tile"
data-index={tile.index}
style={{...style}}
><div className="Tile-Shape"/></div>;
};
const generateTiles = function (tileOrder, animationSeeds) {
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 tileOrder.map(order => {
const tile = Object.assign({},
tiles[order],
{ index: index++, left: x, top: y});
const volcanoActive = 'volcano' in rules
&& rules[`volcano`].enabled;
if ('tiles-start-facing-down' in rules
&& rules[`tiles-start-facing-down`].enabled
&& state !== 'normal'
&& state !== 'volcano'
&& state !== 'winner'
&& (!volcanoActive || tile.type !== 'desert')) {
tile.type = 'jungle';
tile.card = 0;
}
if (volcanoActive
&& tile.type === 'desert') {
tile.type = 'volcano';
tile.card = 0;
}
let div;
if (tile.type === 'wheat') {
div = <div key={`tile-${order}`}>
<Flock count={Math.floor(1 + animationSeeds[index] * 4)}
style={{
top: `${tile.top - tileImageHeight * 0.5}px`,
left: `${tile.left - tileImageWidth * 0.5}px`,
width: `${tileImageWidth}px`,
height: `${tileImageHeight}px`
}}/><Tile
tile={tile}
/></div>;
} else if (tile.type === 'sheep') {
div = <div key={`tile-${order}`}>
<Herd count={Math.floor(1 + animationSeeds[index] * 4)}
style={{
top: `${tile.top - tileImageHeight * 0.5}px`,
left: `${tile.left - tileImageWidth * 0.5}px`,
width: `${tileImageWidth}px`,
height: `${tileImageHeight}px`
}} /><Tile
tile={tile}
/></div>;
} else {
div = <Tile
key={`tile-${order}`}
tile={tile}
/>;
};
if (++rowCount === rows[row]) {
row++;
rowCount = 0;
y += tileWidth;
x = - (rows[row] - 1) * 0.5 * tileHeight;
} else {
x += tileHeight;
}
return div;
});
};
const showTooltip = () => {
document.querySelector('.Board .Tooltip').style.display = 'flex';
}
const clearTooltip = () => {
document.querySelector('.Board .Tooltip').style.display = 'none';
}
const calculateBorderSlot = (side, e) => {
const borderBox = document.querySelector('.Borders').getBoundingClientRect();
let angle = (360 + Math.floor(90 + Math.atan2(e.pageY - borderBox.top, e.pageX - borderBox.left) * 180 / Math.PI)) % 360 - (side * 60);
if (angle > 180) {
angle = angle - 360;
}
let slot = 0;
if (angle > -20 && angle < 5) {
slot = 1;
} else if (angle > 5) {
slot = 2;
}
return slot;
}
const mouseEnter = (border, side, e) => {
const slot = calculateBorderSlot(side, e);
if (!border[slot]) {
clearTooltip();
return;
}
const tooltip = document.querySelector('.Board .Tooltip');
tooltip.textContent = border[slot] === 'bank'
? '3 of one kind for 1 resource' :
`2 ${border[slot]} for 1 resource`;
tooltip.style.top = `${e.pageY}px`;
tooltip.style.left = `${e.pageX + 16}px`;
showTooltip();
};
const mouseMove = (border, side, e) => {
const slot = calculateBorderSlot(side, e);
if (!border[slot]) {
clearTooltip();
return;
}
const tooltip = document.querySelector('.Board .Tooltip');
tooltip.textContent = border[slot] === 'bank'
? '3 of one kind for 1 resource' :
`2 ${border[slot]} for 1 resource`;
tooltip.style.top = `${e.pageY}px`;
tooltip.style.left = `${e.pageX + 16}px`;
showTooltip();
}
const mouseLeave = (border, e) => {
clearTooltip();
};
const generateBorders = function(borderOrder) {
const sides = 6;
let side = -1;
return borderOrder.map(order => {
const border = 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`;
const value = side;
return <div
key={`border-${order}`}
className="Border"
border={border}
onMouseEnter={(e) => { mouseEnter(border, value, e) }}
onMouseMove={(e) => { mouseMove(border, value, e) }}
onMouseLeave={mouseLeave}
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 (borders && borderOrder) {
console.log(`board - Generate board - borders`);
setBorderElements(generateBorders(borderOrder));
}
if (tiles && tileOrder && animationSeeds) {
console.log(`board - Generate board - tiles`);
setTileElements(generateTiles(tileOrder, animationSeeds));
}
/* Regenerate pips every time; it uses ws */
if (pips && pipOrder) {
console.log(`board - Generate board - pips`);
setPipElements(generatePips(pipOrder));
}
if (signature && signature !== generated) {
console.log(`board - Regnerating for ${signature}`);
setGenerated(signature);
}
}, [
signature, generated,
pips, pipOrder, borders, borderOrder, tiles, tileOrder,
animationSeeds,
ws, state, rules
]);
/* Re-render turn info after every render */
useEffect(() => {
if (!turn) { return; }
let nodes = document.querySelectorAll('.Active');
for (let i = 0; i < nodes.length; i++) {
nodes[i].classList.remove('Active');
}
if (turn.roll) {
nodes = document.querySelectorAll(`.Pip[data-roll="${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');
}
}
});
/* Re-render placements after every render */
useEffect(() => {
if (!placements) { return; }
/* Set color and type based on placement data from the server */
placements.corners.forEach((corner, index) => {
const el = document.querySelector(`.Corner[data-index="${index}"]`);
if (!el) { return; }
if (turn.volcano === index) {
el.classList.add('Lava');
} else {
el.classList.remove('Lava');
}
if (!corner.color) {
el.removeAttribute('data-color');
el.removeAttribute('data-type');
} else {
el.setAttribute('data-color', corner.color);
el.setAttribute('data-type', corner.type);
}
});
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 === 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 turn.limits */
if (turn && turn.limits) {
if (turn.limits['roads']) {
turn.limits['roads'].forEach(index => {
const el = document.querySelector(`.Road[data-index="${index}"]`);
if (!el) { return; }
el.classList.add('Option');
el.setAttribute('data-color', turn.color);
});
}
if (turn.limits['corners']) {
turn.limits['corners'].forEach(index => {
const el = document.querySelector(`.Corner[data-index="${index}"]`);
if (!el) { return; }
el.classList.add('Option');
el.setAttribute('data-color', turn.color);
});
}
if (turn.limits['tiles']) {
turn.limits['tiles'].forEach(index => {
const el = document.querySelector(`.Tile[data-index="${index}"]`);
if (!el) { return; }
el.classList.add('Option');
});
}
if (turn.limits['pips']) {
turn.limits['pips'].forEach(index => {
const el = document.querySelector(`.Pip[data-index="${index}"]`);
if (!el) { return; }
el.classList.add('Option');
});
}
}
/* Clear the robber */
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)
);
}
/* Place the robber */
if (robber !== undefined) {
const el = document.querySelector(`.Pip[data-index="${robber}"]`);
if (el) {
el.classList.add('Robber');
el.classList.add(robberName);
}
}
});
const canAction = (action) => {
return (turn && Array.isArray(turn.actions) && turn.actions.indexOf(action) !== -1);
};
const canRoad = (canAction('place-road')
&& turn.color === color
&& (state === 'initial-placement' || state === 'normal'));
const canCorner = ((canAction('place-settlement') || canAction('place-city'))
&& turn.color === color
&& (state === 'initial-placement' || state === 'normal'));
const canPip = (canAction('place-robber')
&& turn.color === color
&& (state === 'initial-placement' || state === 'normal'));
console.log(`board - tile elements`, tileElements);
return (
<div className="Board" ref={board}>
<div className="Tooltip">tooltip</div>
<div className="BoardBox">
<div className="Borders" disabled>
{ borderElements }
</div>
<div className="Tiles" disabled>
{ tileElements }
</div>
<div className="Pips" disabled={!canPip}>
{ pipElements }
</div>
<div className="Corners" disabled={!canCorner}>
{ cornerElements }
</div>
<div className="Roads" disabled={!canRoad}>
{ roadElements }
</div>
</div>
</div>
);
};
export { Board };